diff options
Diffstat (limited to 'packages/astro')
72 files changed, 852 insertions, 1048 deletions
diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index a551fa323..e026a0eed 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -327,6 +327,28 @@ - @astrojs/internal-helpers@0.2.0-beta.0 - @astrojs/markdown-remark@3.0.0-beta.0 +## 2.10.9 + +### Patch Changes + +- [#8091](https://github.com/withastro/astro/pull/8091) [`56e7c5177`](https://github.com/withastro/astro/commit/56e7c5177bd61b404978dc9b82e2d34d76a4b2f9) Thanks [@martrapp](https://github.com/martrapp)! - Handle `<noscript>` tags in `<head>` during ViewTransitions + +## 2.10.8 + +### Patch Changes + +- [#7702](https://github.com/withastro/astro/pull/7702) [`c19987df0`](https://github.com/withastro/astro/commit/c19987df0be3520cf774476cea270c03edd08354) Thanks [@shishkin](https://github.com/shishkin)! - Fix AstroConfigSchema type export + +- [#8084](https://github.com/withastro/astro/pull/8084) [`560e45924`](https://github.com/withastro/astro/commit/560e45924622141206ff5b47d134cb343d6d2a71) Thanks [@hbgl](https://github.com/hbgl)! - Stream request body instead of buffering it in memory. + +- [#8066](https://github.com/withastro/astro/pull/8066) [`afc45af20`](https://github.com/withastro/astro/commit/afc45af2022f7c43fbb6c5c04983695f3819e47e) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Add support for non-awaited imports to the Image component and `getImage` + +- [#7866](https://github.com/withastro/astro/pull/7866) [`d1f7143f9`](https://github.com/withastro/astro/commit/d1f7143f9caf2ffa0e87cc55c0e05339d3501db3) Thanks [@43081j](https://github.com/43081j)! - Add second type argument to the AstroGlobal type to type Astro.self. This change will ultimately allow our editor tooling to provide props completions and intellisense for `<Astro.self />` + +- [#8032](https://github.com/withastro/astro/pull/8032) [`3e46634fd`](https://github.com/withastro/astro/commit/3e46634fd540e5b967d2e5c9abd6235452cee2f2) Thanks [@natemoo-re](https://github.com/natemoo-re)! - `astro add` now passes down `--save-prod`, `--save-dev`, `--save-exact`, and `--no-save` flags for installation + +- [#8035](https://github.com/withastro/astro/pull/8035) [`a12027b6a`](https://github.com/withastro/astro/commit/a12027b6af411be39700919ca47e240a335e9887) Thanks [@fyndor](https://github.com/fyndor)! - Removed extra double quotes from computed style in shiki code component + ## 2.10.7 ### Patch Changes diff --git a/packages/astro/components/Code.astro b/packages/astro/components/Code.astro index a990e877a..ee7a84a09 100644 --- a/packages/astro/components/Code.astro +++ b/packages/astro/components/Code.astro @@ -74,9 +74,9 @@ const html = renderToHtml(tokens, { // Handle code wrapping // if wrap=null, do nothing. if (wrap === false) { - style += '; overflow-x: auto;"'; + style += '; overflow-x: auto;'; } else if (wrap === true) { - style += '; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"'; + style += '; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;'; } return `<${tag} class="${className}" style="${style}" tabindex="0">${children}</${tag}>`; }, diff --git a/packages/astro/components/ViewTransitions.astro b/packages/astro/components/ViewTransitions.astro index 05fc57aa6..47eb3c112 100644 --- a/packages/astro/components/ViewTransitions.astro +++ b/packages/astro/components/ViewTransitions.astro @@ -38,12 +38,21 @@ const { fallback = 'animate' } = Astro.props as Props; const throttle = (cb: (...args: any[]) => any, delay: number) => { let wait = false; + // During the waiting time additional events are lost. + // So repeat the callback at the end if we have swallowed events. + let onceMore = false; return (...args: any[]) => { - if (wait) return; - + if (wait) { + onceMore = true; + return; + } cb(...args); wait = true; setTimeout(() => { + if (onceMore) { + onceMore = false; + cb(...args); + } wait = false; }, delay); }; @@ -125,6 +134,10 @@ const { fallback = 'animate' } = Astro.props as Props; }; const swap = () => { + // noscript tags inside head element are not honored on swap (#7969). + // Remove them before swapping. + doc.querySelectorAll('head noscript').forEach((el) => el.remove()); + // Swap head for (const el of Array.from(document.head.children)) { const newEl = persistedHeadElement(el); @@ -159,6 +172,8 @@ const { fallback = 'animate' } = Astro.props as Props; } if (state?.scrollY != null) { scrollTo(0, state.scrollY); + // Overwrite erroneous updates by the scroll handler during transition + persistState(state); } triggerEvent('astro:beforeload'); diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index e2b3e6d63..f9568d417 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -13,6 +13,7 @@ import type { AddressInfo } from 'node:net'; import type * as rollup from 'rollup'; import type { TsConfigJson } from 'tsconfig-resolver'; import type * as vite from 'vite'; +import type { RemotePattern } from '../assets/utils/remotePattern'; import type { SerializedSSRManifest } from '../core/app/types'; import type { PageBuildData } from '../core/build/types'; import type { AstroConfigType } from '../core/config'; @@ -45,6 +46,7 @@ export type { ImageQualityPreset, ImageTransform, } from '../assets/types'; +export type { RemotePattern } from '../assets/utils/remotePattern'; export type { SSRManifest } from '../core/app/types'; export type { AstroCookies } from '../core/cookies'; @@ -367,10 +369,10 @@ export interface ViteUserConfig extends vite.UserConfig { ssr?: vite.SSROptions; } -export interface ImageServiceConfig { +export interface ImageServiceConfig<T extends Record<string, any> = Record<string, any>> { // eslint-disable-next-line @typescript-eslint/ban-types entrypoint: 'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | (string & {}); - config?: Record<string, any>; + config?: T; } /** @@ -697,6 +699,10 @@ export interface AstroUserConfig { * - `file` - The `Astro.url.pathname` will include `.html`; ie `/foo.html`. * * This means that when you create relative URLs using `new URL('./relative', Astro.url)`, you will get consistent behavior between dev and build. + * + * To prevent inconsistencies with trailing slash behaviour in dev, you can restrict the [`trailingSlash` option](#trailingslash) to `'always'` or `'never'` depending on your build format: + * - `directory` - Set `trailingSlash: 'always'` + * - `file` - Set `trailingSlash: 'never'` */ format?: 'file' | 'directory'; /** @@ -833,10 +839,10 @@ export interface AstroUserConfig { * @default `never` * @version 2.6.0 * @description - * Control whether styles are sent to the browser in a separate css file or inlined into `<style>` tags. Choose from the following options: - * - `'always'` - all styles are inlined into `<style>` tags - * - `'auto'` - only stylesheets smaller than `ViteConfig.build.assetsInlineLimit` (default: 4kb) are inlined. Otherwise, styles are sent in external stylesheets. - * - `'never'` - all styles are sent in external stylesheets + * Control whether project styles are sent to the browser in a separate css file or inlined into `<style>` tags. Choose from the following options: + * - `'always'` - project styles are inlined into `<style>` tags + * - `'auto'` - only stylesheets smaller than `ViteConfig.build.assetsInlineLimit` (default: 4kb) are inlined. Otherwise, project styles are sent in external stylesheets. + * - `'never'` - project styles are sent in external stylesheets * * ```js * { @@ -1004,6 +1010,68 @@ export interface AstroUserConfig { * ``` */ service: ImageServiceConfig; + + /** + * @docs + * @name image.domains (Experimental) + * @type {string[]} + * @default `{domains: []}` + * @version 2.10.10 + * @description + * Defines a list of permitted image source domains for local image optimization. No other remote images will be optimized by Astro. + * + * This option requires an array of individual domain names as strings. Wildcards are not permitted. Instead, use [`image.remotePatterns`](#imageremotepatterns-experimental) to define a list of allowed source URL patterns. + * + * ```js + * // astro.config.mjs + * { + * image: { + * // Example: Allow remote image optimization from a single domain + * domains: ['astro.build'], + * }, + * } + * ``` + */ + domains?: string[]; + + /** + * @docs + * @name image.remotePatterns (Experimental) + * @type {RemotePattern[]} + * @default `{remotePatterns: []}` + * @version 2.10.10 + * @description + * Defines a list of permitted image source URL patterns for local image optimization. + * + * `remotePatterns` can be configured with four properties: + * 1. protocol + * 2. hostname + * 3. port + * 4. pathname + * + * ```js + * { + * image: { + * // Example: allow processing all images from your aws s3 bucket + * remotePatterns: [{ + * protocol: 'https', + * hostname: '**.amazonaws.com', + * }], + * }, + * } + * ``` + * + * You can use wildcards to define the permitted `hostname` and `pathname` values as described below. Otherwise, only the exact values provided will be configured: + * `hostname`: + * - Start with '**.' to allow all subdomains ('endsWith'). + * - Start with '*.' to allow only one level of subdomain. + * + * `pathname`: + * - End with '/**' to allow all sub-routes ('startsWith'). + * - End with '/*' to allow only one level of sub-route. + + */ + remotePatterns?: Partial<RemotePattern>[]; }; /** diff --git a/packages/astro/src/assets/build/generate.ts b/packages/astro/src/assets/build/generate.ts new file mode 100644 index 000000000..b78800a43 --- /dev/null +++ b/packages/astro/src/assets/build/generate.ts @@ -0,0 +1,174 @@ +import fs, { readFileSync } from 'node:fs'; +import { basename, join } from 'node:path/posix'; +import type { StaticBuildOptions } from '../../core/build/types.js'; +import { warn } from '../../core/logger/core.js'; +import { prependForwardSlash } from '../../core/path.js'; +import { isServerLikeOutput } from '../../prerender/utils.js'; +import { getConfiguredImageService, isESMImportedImage } from '../internal.js'; +import type { LocalImageService } from '../services/service.js'; +import type { ImageMetadata, ImageTransform } from '../types.js'; +import { loadRemoteImage, type RemoteCacheEntry } from './remote.js'; + +interface GenerationDataUncached { + cached: false; + weight: { + before: number; + after: number; + }; +} + +interface GenerationDataCached { + cached: true; +} + +type GenerationData = GenerationDataUncached | GenerationDataCached; + +export async function generateImage( + buildOpts: StaticBuildOptions, + options: ImageTransform, + filepath: string +): Promise<GenerationData | undefined> { + let useCache = true; + const assetsCacheDir = new URL('assets/', buildOpts.settings.config.cacheDir); + + // Ensure that the cache directory exists + try { + await fs.promises.mkdir(assetsCacheDir, { recursive: true }); + } catch (err) { + warn( + buildOpts.logging, + 'astro:assets', + `An error was encountered while creating the cache directory. Proceeding without caching. Error: ${err}` + ); + useCache = false; + } + + let serverRoot: URL, clientRoot: URL; + if (isServerLikeOutput(buildOpts.settings.config)) { + serverRoot = buildOpts.settings.config.build.server; + clientRoot = buildOpts.settings.config.build.client; + } else { + serverRoot = buildOpts.settings.config.outDir; + clientRoot = buildOpts.settings.config.outDir; + } + + const isLocalImage = isESMImportedImage(options.src); + + const finalFileURL = new URL('.' + filepath, clientRoot); + const finalFolderURL = new URL('./', finalFileURL); + + // For remote images, instead of saving the image directly, we save a JSON file with the image data and expiration date from the server + const cacheFile = basename(filepath) + (isLocalImage ? '' : '.json'); + const cachedFileURL = new URL(cacheFile, assetsCacheDir); + + await fs.promises.mkdir(finalFolderURL, { recursive: true }); + + // Check if we have a cached entry first + try { + if (isLocalImage) { + await fs.promises.copyFile(cachedFileURL, finalFileURL); + + return { + cached: true, + }; + } else { + const JSONData = JSON.parse(readFileSync(cachedFileURL, 'utf-8')) as RemoteCacheEntry; + + // If the cache entry is not expired, use it + if (JSONData.expires < Date.now()) { + await fs.promises.writeFile(finalFileURL, Buffer.from(JSONData.data, 'base64')); + + return { + cached: true, + }; + } + } + } catch (e: any) { + if (e.code !== 'ENOENT') { + throw new Error(`An error was encountered while reading the cache file. Error: ${e}`); + } + // If the cache file doesn't exist, just move on, and we'll generate it + } + + // The original filepath or URL from the image transform + const originalImagePath = isLocalImage + ? (options.src as ImageMetadata).src + : (options.src as string); + + let imageData; + let resultData: { data: Buffer | undefined; expires: number | undefined } = { + data: undefined, + expires: undefined, + }; + + // If the image is local, we can just read it directly, otherwise we need to download it + if (isLocalImage) { + imageData = await fs.promises.readFile( + new URL( + '.' + + prependForwardSlash( + join(buildOpts.settings.config.build.assets, basename(originalImagePath)) + ), + serverRoot + ) + ); + } else { + const remoteImage = await loadRemoteImage(originalImagePath); + resultData.expires = remoteImage.expires; + imageData = remoteImage.data; + } + + const imageService = (await getConfiguredImageService()) as LocalImageService; + resultData.data = ( + await imageService.transform( + imageData, + { ...options, src: originalImagePath }, + buildOpts.settings.config.image + ) + ).data; + + try { + // Write the cache entry + if (useCache) { + if (isLocalImage) { + await fs.promises.writeFile(cachedFileURL, resultData.data); + } else { + await fs.promises.writeFile( + cachedFileURL, + JSON.stringify({ + data: Buffer.from(resultData.data).toString('base64'), + expires: resultData.expires, + }) + ); + } + } + } catch (e) { + warn( + buildOpts.logging, + 'astro:assets', + `An error was encountered while creating the cache directory. Proceeding without caching. Error: ${e}` + ); + } finally { + // Write the final file + await fs.promises.writeFile(finalFileURL, resultData.data); + } + + return { + cached: false, + weight: { + // Divide by 1024 to get size in kilobytes + before: Math.trunc(imageData.byteLength / 1024), + after: Math.trunc(Buffer.from(resultData.data).byteLength / 1024), + }, + }; +} + +export function getStaticImageList(): Iterable< + [string, { path: string; options: ImageTransform }] +> { + if (!globalThis?.astroAsset?.staticImages) { + return []; + } + + return globalThis.astroAsset.staticImages?.entries(); +} diff --git a/packages/astro/src/assets/build/remote.ts b/packages/astro/src/assets/build/remote.ts new file mode 100644 index 000000000..c3d4bb9ba --- /dev/null +++ b/packages/astro/src/assets/build/remote.ts @@ -0,0 +1,48 @@ +import CachePolicy from 'http-cache-semantics'; + +export type RemoteCacheEntry = { data: string; expires: number }; + +export async function loadRemoteImage(src: string) { + const req = new Request(src); + const res = await fetch(req); + + if (!res.ok) { + throw new Error( + `Failed to load remote image ${src}. The request did not return a 200 OK response. (received ${res.status}))` + ); + } + + // calculate an expiration date based on the response's TTL + const policy = new CachePolicy(webToCachePolicyRequest(req), webToCachePolicyResponse(res)); + const expires = policy.storable() ? policy.timeToLive() : 0; + + return { + data: Buffer.from(await res.arrayBuffer()), + expires: Date.now() + expires, + }; +} + +function webToCachePolicyRequest({ url, method, headers: _headers }: Request): CachePolicy.Request { + let headers: CachePolicy.Headers = {}; + // Be defensive here due to a cookie header bug in node@18.14.1 + undici + try { + headers = Object.fromEntries(_headers.entries()); + } catch {} + return { + method, + url, + headers, + }; +} + +function webToCachePolicyResponse({ status, headers: _headers }: Response): CachePolicy.Response { + let headers: CachePolicy.Headers = {}; + // Be defensive here due to a cookie header bug in node@18.14.1 + undici + try { + headers = Object.fromEntries(_headers.entries()); + } catch {} + return { + status, + headers, + }; +} diff --git a/packages/astro/src/assets/generate.ts b/packages/astro/src/assets/generate.ts deleted file mode 100644 index 04488ed8f..000000000 --- a/packages/astro/src/assets/generate.ts +++ /dev/null @@ -1,132 +0,0 @@ -import fs from 'node:fs'; -import { basename, join } from 'node:path/posix'; -import type { StaticBuildOptions } from '../core/build/types.js'; -import { warn } from '../core/logger/core.js'; -import { prependForwardSlash } from '../core/path.js'; -import { isServerLikeOutput } from '../prerender/utils.js'; -import { getConfiguredImageService, isESMImportedImage } from './internal.js'; -import type { LocalImageService } from './services/service.js'; -import type { ImageTransform } from './types.js'; - -interface GenerationDataUncached { - cached: false; - weight: { - before: number; - after: number; - }; -} - -interface GenerationDataCached { - cached: true; -} - -type GenerationData = GenerationDataUncached | GenerationDataCached; - -export async function generateImage( - buildOpts: StaticBuildOptions, - options: ImageTransform, - filepath: string -): Promise<GenerationData | undefined> { - if (typeof buildOpts.settings.config.image === 'undefined') { - throw new Error( - "Astro hasn't set a default service for `astro:assets`. This is an internal error and you should report it." - ); - } - if (!isESMImportedImage(options.src)) { - return undefined; - } - - let useCache = true; - const assetsCacheDir = new URL('assets/', buildOpts.settings.config.cacheDir); - - // Ensure that the cache directory exists - try { - await fs.promises.mkdir(assetsCacheDir, { recursive: true }); - } catch (err) { - warn( - buildOpts.logging, - 'astro:assets', - `An error was encountered while creating the cache directory. Proceeding without caching. Error: ${err}` - ); - useCache = false; - } - - let serverRoot: URL, clientRoot: URL; - if (isServerLikeOutput(buildOpts.settings.config)) { - serverRoot = buildOpts.settings.config.build.server; - clientRoot = buildOpts.settings.config.build.client; - } else { - serverRoot = buildOpts.settings.config.outDir; - clientRoot = buildOpts.settings.config.outDir; - } - - const finalFileURL = new URL('.' + filepath, clientRoot); - const finalFolderURL = new URL('./', finalFileURL); - const cachedFileURL = new URL(basename(filepath), assetsCacheDir); - - try { - await fs.promises.copyFile(cachedFileURL, finalFileURL); - - return { - cached: true, - }; - } catch (e) { - // no-op - } - - // The original file's path (the `src` attribute of the ESM imported image passed by the user) - const originalImagePath = options.src.src; - - const fileData = await fs.promises.readFile( - new URL( - '.' + - prependForwardSlash( - join(buildOpts.settings.config.build.assets, basename(originalImagePath)) - ), - serverRoot - ) - ); - - const imageService = (await getConfiguredImageService()) as LocalImageService; - const resultData = await imageService.transform( - fileData, - { ...options, src: originalImagePath }, - buildOpts.settings.config.image.service.config - ); - - await fs.promises.mkdir(finalFolderURL, { recursive: true }); - - if (useCache) { - try { - await fs.promises.writeFile(cachedFileURL, resultData.data); - await fs.promises.copyFile(cachedFileURL, finalFileURL); - } catch (e) { - warn( - buildOpts.logging, - 'astro:assets', - `An error was encountered while creating the cache directory. Proceeding without caching. Error: ${e}` - ); - await fs.promises.writeFile(finalFileURL, resultData.data); - } - } else { - await fs.promises.writeFile(finalFileURL, resultData.data); - } - - return { - cached: false, - weight: { - before: Math.trunc(fileData.byteLength / 1024), - after: Math.trunc(resultData.data.byteLength / 1024), - }, - }; -} - -export function getStaticImageList(): Iterable< - [string, { path: string; options: ImageTransform }] -> { - if (!globalThis?.astroAsset?.staticImages) { - return []; - } - - return globalThis.astroAsset.staticImages?.entries(); -} diff --git a/packages/astro/src/assets/image-endpoint.ts b/packages/astro/src/assets/image-endpoint.ts index d9a101679..d83517379 100644 --- a/packages/astro/src/assets/image-endpoint.ts +++ b/packages/astro/src/assets/image-endpoint.ts @@ -1,8 +1,10 @@ import mime from 'mime/lite.js'; import type { APIRoute } from '../@types/astro.js'; import { etag } from './utils/etag.js'; +import { isRemotePath } from '../core/path.js'; +import { getConfiguredImageService, isRemoteAllowed } from './internal.js'; // @ts-expect-error -import { getConfiguredImageService, imageServiceConfig } from 'astro:assets'; +import { imageConfig } from 'astro:assets'; async function loadRemoteImage(src: URL) { try { @@ -30,7 +32,7 @@ export const GET: APIRoute = async ({ request }) => { } const url = new URL(request.url); - const transform = await imageService.parseURL(url, imageServiceConfig); + const transform = await imageService.parseURL(url, imageConfig); if (!transform?.src) { throw new Error('Incorrect transform returned by `parseURL`'); @@ -42,17 +44,18 @@ export const GET: APIRoute = async ({ request }) => { const sourceUrl = isRemotePath(transform.src) ? new URL(transform.src) : new URL(transform.src, url.origin); + + if (isRemotePath(transform.src) && isRemoteAllowed(transform.src, imageConfig) === false) { + return new Response('Forbidden', { status: 403 }); + } + inputBuffer = await loadRemoteImage(sourceUrl); if (!inputBuffer) { return new Response('Not Found', { status: 404 }); } - const { data, format } = await imageService.transform( - inputBuffer, - transform, - imageServiceConfig - ); + const { data, format } = await imageService.transform(inputBuffer, transform, imageConfig); return new Response(data, { status: 200, diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts index a49828a46..dd5e427f6 100644 --- a/packages/astro/src/assets/internal.ts +++ b/packages/astro/src/assets/internal.ts @@ -1,4 +1,5 @@ -import type { AstroSettings } from '../@types/astro.js'; +import { isRemotePath } from '@astrojs/internal-helpers/path'; +import type { AstroConfig, AstroSettings } from '../@types/astro.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { isLocalService, type ImageService } from './services/service.js'; import type { @@ -7,6 +8,7 @@ import type { ImageTransform, UnresolvedImageTransform, } from './types.js'; +import { matchHostname, matchPattern } from './utils/remotePattern.js'; export function injectImageEndpoint(settings: AstroSettings) { // TODO: Add a setting to disable the image endpoint @@ -23,6 +25,26 @@ export function isESMImportedImage(src: ImageMetadata | string): src is ImageMet return typeof src === 'object'; } +export function isRemoteImage(src: ImageMetadata | string): src is string { + return typeof src === 'string'; +} + +export function isRemoteAllowed( + src: string, + { + domains = [], + remotePatterns = [], + }: Partial<Pick<AstroConfig['image'], 'domains' | 'remotePatterns'>> +): boolean { + if (!isRemotePath(src)) return false; + + const url = new URL(src); + return ( + domains.some((domain) => matchHostname(url, domain)) || + remotePatterns.some((remotePattern) => matchPattern(url, remotePattern)) + ); +} + export async function getConfiguredImageService(): Promise<ImageService> { if (!globalThis?.astroAsset?.imageService) { const { default: service }: { default: ImageService } = await import( @@ -44,7 +66,7 @@ export async function getConfiguredImageService(): Promise<ImageService> { export async function getImage( options: ImageTransform | UnresolvedImageTransform, - serviceConfig: Record<string, any> + imageConfig: AstroConfig['image'] ): Promise<GetImageResult> { if (!options || typeof options !== 'object') { throw new AstroError({ @@ -65,13 +87,18 @@ export async function getImage( }; const validatedOptions = service.validateOptions - ? await service.validateOptions(resolvedOptions, serviceConfig) + ? await service.validateOptions(resolvedOptions, imageConfig) : resolvedOptions; - let imageURL = await service.getURL(validatedOptions, serviceConfig); + let imageURL = await service.getURL(validatedOptions, imageConfig); // In build and for local services, we need to collect the requested parameters so we can generate the final images - if (isLocalService(service) && globalThis.astroAsset.addStaticImage) { + if ( + isLocalService(service) && + globalThis.astroAsset.addStaticImage && + // If `getURL` returned the same URL as the user provided, it means the service doesn't need to do anything + !(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src) + ) { imageURL = globalThis.astroAsset.addStaticImage(validatedOptions); } @@ -81,7 +108,7 @@ export async function getImage( src: imageURL, attributes: service.getHTMLAttributes !== undefined - ? service.getHTMLAttributes(validatedOptions, serviceConfig) + ? service.getHTMLAttributes(validatedOptions, imageConfig) : {}, }; } diff --git a/packages/astro/src/assets/services/service.ts b/packages/astro/src/assets/services/service.ts index d3479c880..5af4a898b 100644 --- a/packages/astro/src/assets/services/service.ts +++ b/packages/astro/src/assets/services/service.ts @@ -1,7 +1,8 @@ +import type { AstroConfig } from '../../@types/astro.js'; import { AstroError, AstroErrorData } from '../../core/errors/index.js'; import { joinPaths } from '../../core/path.js'; import { VALID_SUPPORTED_FORMATS } from '../consts.js'; -import { isESMImportedImage } from '../internal.js'; +import { isESMImportedImage, isRemoteAllowed } from '../internal.js'; import type { ImageOutputFormat, ImageTransform } from '../types.js'; export type ImageService = LocalImageService | ExternalImageService; @@ -23,7 +24,11 @@ export function parseQuality(quality: string): string | number { return result; } -interface SharedServiceProps { +type ImageConfig<T> = Omit<AstroConfig['image'], 'service'> & { + service: { entrypoint: string; config: T }; +}; + +interface SharedServiceProps<T extends Record<string, any> = Record<string, any>> { /** * Return the URL to the endpoint or URL your images are generated from. * @@ -32,7 +37,7 @@ interface SharedServiceProps { * For external services, this should point to the URL your images are coming from, for instance, `/_vercel/image` * */ - getURL: (options: ImageTransform, serviceConfig: Record<string, any>) => string | Promise<string>; + getURL: (options: ImageTransform, imageConfig: ImageConfig<T>) => string | Promise<string>; /** * Return any additional HTML attributes separate from `src` that your service requires to show the image properly. * @@ -41,7 +46,7 @@ interface SharedServiceProps { */ getHTMLAttributes?: ( options: ImageTransform, - serviceConfig: Record<string, any> + imageConfig: ImageConfig<T> ) => Record<string, any> | Promise<Record<string, any>>; /** * Validate and return the options passed by the user. @@ -53,18 +58,20 @@ interface SharedServiceProps { */ validateOptions?: ( options: ImageTransform, - serviceConfig: Record<string, any> + imageConfig: ImageConfig<T> ) => ImageTransform | Promise<ImageTransform>; } -export type ExternalImageService = SharedServiceProps; +export type ExternalImageService<T extends Record<string, any> = Record<string, any>> = + SharedServiceProps<T>; export type LocalImageTransform = { src: string; [key: string]: any; }; -export interface LocalImageService extends SharedServiceProps { +export interface LocalImageService<T extends Record<string, any> = Record<string, any>> + extends SharedServiceProps<T> { /** * Parse the requested parameters passed in the URL from `getURL` back into an object to be used later by `transform`. * @@ -72,7 +79,7 @@ export interface LocalImageService extends SharedServiceProps { */ parseURL: ( url: URL, - serviceConfig: Record<string, any> + imageConfig: ImageConfig<T> ) => LocalImageTransform | undefined | Promise<LocalImageTransform> | Promise<undefined>; /** * Performs the image transformations on the input image and returns both the binary data and @@ -81,7 +88,7 @@ export interface LocalImageService extends SharedServiceProps { transform: ( inputBuffer: Buffer, transform: LocalImageTransform, - serviceConfig: Record<string, any> + imageConfig: ImageConfig<T> ) => Promise<{ data: Buffer; format: ImageOutputFormat }>; } @@ -202,21 +209,31 @@ export const baseService: Omit<LocalImageService, 'transform'> = { decoding: attributes.decoding ?? 'async', }; }, - getURL(options: ImageTransform) { - // Both our currently available local services don't handle remote images, so we return the path as is. - if (!isESMImportedImage(options.src)) { + getURL(options, imageConfig) { + const searchParams = new URLSearchParams(); + + if (isESMImportedImage(options.src)) { + searchParams.append('href', options.src.src); + } else if (isRemoteAllowed(options.src, imageConfig)) { + searchParams.append('href', options.src); + } else { + // If it's not an imported image, nor is it allowed using the current domains or remote patterns, we'll just return the original URL return options.src; } - const searchParams = new URLSearchParams(); - searchParams.append('href', options.src.src); + const params: Record<string, keyof typeof options> = { + w: 'width', + h: 'height', + q: 'quality', + f: 'format', + }; - options.width && searchParams.append('w', options.width.toString()); - options.height && searchParams.append('h', options.height.toString()); - options.quality && searchParams.append('q', options.quality.toString()); - options.format && searchParams.append('f', options.format); + Object.entries(params).forEach(([param, key]) => { + options[key] && searchParams.append(param, options[key].toString()); + }); - return joinPaths(import.meta.env.BASE_URL, '/_image?') + searchParams; + const imageEndpoint = joinPaths(import.meta.env.BASE_URL, '/_image'); + return `${imageEndpoint}?${searchParams}`; }, parseURL(url) { const params = url.searchParams; diff --git a/packages/astro/src/assets/utils/remotePattern.ts b/packages/astro/src/assets/utils/remotePattern.ts new file mode 100644 index 000000000..7708b42e7 --- /dev/null +++ b/packages/astro/src/assets/utils/remotePattern.ts @@ -0,0 +1,63 @@ +export type RemotePattern = { + hostname?: string; + pathname?: string; + protocol?: string; + port?: string; +}; + +export function matchPattern(url: URL, remotePattern: RemotePattern) { + return ( + matchProtocol(url, remotePattern.protocol) && + matchHostname(url, remotePattern.hostname, true) && + matchPort(url, remotePattern.port) && + matchPathname(url, remotePattern.pathname, true) + ); +} + +export function matchPort(url: URL, port?: string) { + return !port || port === url.port; +} + +export function matchProtocol(url: URL, protocol?: string) { + return !protocol || protocol === url.protocol.slice(0, -1); +} + +export function matchHostname(url: URL, hostname?: string, allowWildcard?: boolean) { + if (!hostname) { + return true; + } else if (!allowWildcard || !hostname.startsWith('*')) { + return hostname === url.hostname; + } else if (hostname.startsWith('**.')) { + const slicedHostname = hostname.slice(2); // ** length + return slicedHostname !== url.hostname && url.hostname.endsWith(slicedHostname); + } else if (hostname.startsWith('*.')) { + const slicedHostname = hostname.slice(1); // * length + const additionalSubdomains = url.hostname + .replace(slicedHostname, '') + .split('.') + .filter(Boolean); + return additionalSubdomains.length === 1; + } + + return false; +} + +export function matchPathname(url: URL, pathname?: string, allowWildcard?: boolean) { + if (!pathname) { + return true; + } else if (!allowWildcard || !pathname.endsWith('*')) { + return pathname === url.pathname; + } else if (pathname.endsWith('/**')) { + const slicedPathname = pathname.slice(0, -2); // ** length + return slicedPathname !== url.pathname && url.pathname.startsWith(slicedPathname); + } else if (pathname.endsWith('/*')) { + const slicedPathname = pathname.slice(0, -1); // * length + const additionalPathChunks = url.pathname + .replace(slicedPathname, '') + .split('/') + .filter(Boolean); + return additionalPathChunks.length === 1; + } + + return false; +} diff --git a/packages/astro/src/assets/utils/transformToPath.ts b/packages/astro/src/assets/utils/transformToPath.ts index 04ddee0a1..d5535137b 100644 --- a/packages/astro/src/assets/utils/transformToPath.ts +++ b/packages/astro/src/assets/utils/transformToPath.ts @@ -5,14 +5,13 @@ import { isESMImportedImage } from '../internal.js'; import type { ImageTransform } from '../types.js'; export function propsToFilename(transform: ImageTransform, hash: string) { - if (!isESMImportedImage(transform.src)) { - return transform.src; - } - - let filename = removeQueryString(transform.src.src); + let filename = removeQueryString( + isESMImportedImage(transform.src) ? transform.src.src : transform.src + ); const ext = extname(filename); filename = basename(filename, ext); - const outputExt = transform.format ? `.${transform.format}` : ext; + + let outputExt = transform.format ? `.${transform.format}` : ext; return `/${filename}_${hash}${outputExt}`; } diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index 24de955ba..f194e5288 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -9,7 +9,6 @@ import { removeQueryString, } from '../core/path.js'; import { VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js'; -import { isESMImportedImage } from './internal.js'; import { emitESMImage } from './utils/emitAsset.js'; import { hashTransform, propsToFilename } from './utils/transformToPath.js'; @@ -45,8 +44,8 @@ export default function assets({ import { getImage as getImageInternal } from "astro/assets"; export { default as Image } from "astro/components/Image.astro"; - export const imageServiceConfig = ${JSON.stringify(settings.config.image.service.config)}; - export const getImage = async (options) => await getImageInternal(options, imageServiceConfig); + export const imageConfig = ${JSON.stringify(settings.config.image)}; + export const getImage = async (options) => await getImageInternal(options, imageConfig); `; } }, @@ -69,15 +68,10 @@ export default function assets({ if (globalThis.astroAsset.staticImages.has(hash)) { filePath = globalThis.astroAsset.staticImages.get(hash)!.path; } else { - // If the image is not imported, we can return the path as-is, since static references - // should only point ot valid paths for builds or remote images - if (!isESMImportedImage(options.src)) { - return options.src; - } - filePath = prependForwardSlash( joinPaths(settings.config.build.assets, propsToFilename(options, hash)) ); + globalThis.astroAsset.staticImages.set(hash, { path: filePath, options: options }); } diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index c3b687d01..92f671b85 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -258,10 +258,19 @@ export class App { const errorRouteData = matchRoute('/' + status, this.#manifestData); const url = new URL(request.url); if (errorRouteData) { - if (errorRouteData.prerender && !errorRouteData.route.endsWith(`/${status}`)) { - const statusURL = new URL(`${this.#baseWithoutTrailingSlash}/${status}`, url); + if (errorRouteData.prerender) { + const maybeDotHtml = errorRouteData.route.endsWith(`/${status}`) ? '.html' : ''; + const statusURL = new URL( + `${this.#baseWithoutTrailingSlash}/${status}${maybeDotHtml}`, + url + ); const response = await fetch(statusURL.toString()); - return this.#mergeResponses(response, originalResponse); + + // response for /404.html and 500.html is 200, which is not meaningful + // so we create an override + const override = { status }; + + return this.#mergeResponses(response, originalResponse, override); } const mod = await this.#getModuleForRoute(errorRouteData); try { @@ -287,14 +296,31 @@ export class App { return response; } - #mergeResponses(newResponse: Response, oldResponse?: Response) { - if (!oldResponse) return newResponse; - const { status, statusText, headers } = oldResponse; + #mergeResponses(newResponse: Response, oldResponse?: Response, override?: { status: 404 | 500 }) { + if (!oldResponse) { + if (override !== undefined) { + return new Response(newResponse.body, { + status: override.status, + statusText: newResponse.statusText, + headers: newResponse.headers, + }); + } + return newResponse; + } + + const { statusText, headers } = oldResponse; + + // If the the new response did not have a meaningful status, an override may have been provided + // If the original status was 200 (default), override it with the new status (probably 404 or 500) + // Otherwise, the user set a specific status while rendering and we should respect that one + const status = override?.status + ? override.status + : oldResponse.status === 200 + ? newResponse.status + : oldResponse.status; return new Response(newResponse.body, { - // If the original status was 200 (default), override it with the new status (probably 404 or 500) - // Otherwise, the user set a specific status while rendering and we should respect that one - status: status === 200 ? newResponse.status : status, + status, statusText: status === 200 ? newResponse.statusText : statusText, headers: new Headers(Array.from(headers)), }); diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts index f31af36db..054064a08 100644 --- a/packages/astro/src/core/app/node.ts +++ b/packages/astro/src/core/app/node.ts @@ -10,20 +10,33 @@ export { apply as applyPolyfills } from '../polyfill.js'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); -function createRequestFromNodeRequest(req: NodeIncomingMessage, body?: Uint8Array): Request { +type CreateNodeRequestOptions = { + emptyBody?: boolean; +}; + +type BodyProps = Partial<RequestInit>; + +function createRequestFromNodeRequest( + req: NodeIncomingMessage, + options?: CreateNodeRequestOptions +): Request { const protocol = req.socket instanceof TLSSocket || req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http'; const hostname = req.headers.host || req.headers[':authority']; const url = `${protocol}://${hostname}${req.url}`; - const rawHeaders = req.headers as Record<string, any>; - const entries = Object.entries(rawHeaders); + const headers = makeRequestHeaders(req); const method = req.method || 'GET'; + let bodyProps: BodyProps = {}; + const bodyAllowed = method !== 'HEAD' && method !== 'GET' && !options?.emptyBody; + if (bodyAllowed) { + bodyProps = makeRequestBody(req); + } const request = new Request(url, { method, - headers: new Headers(entries), - body: ['HEAD', 'GET'].includes(method) ? null : body, + headers, + ...bodyProps, }); if (req.socket?.remoteAddress) { Reflect.set(request, clientAddressSymbol, req.socket.remoteAddress); @@ -31,63 +44,83 @@ function createRequestFromNodeRequest(req: NodeIncomingMessage, body?: Uint8Arra return request; } -class NodeIncomingMessage extends IncomingMessage { - /** - * The read-only body property of the Request interface contains a ReadableStream with the body contents that have been added to the request. - */ - body?: unknown; +function makeRequestHeaders(req: NodeIncomingMessage): Headers { + const headers = new Headers(); + for (const [name, value] of Object.entries(req.headers)) { + if (value === undefined) { + continue; + } + if (Array.isArray(value)) { + for (const item of value) { + headers.append(name, item); + } + } else { + headers.append(name, value); + } + } + return headers; } -export class NodeApp extends App { - match(req: NodeIncomingMessage | Request, opts: MatchOptions = {}) { - return super.match(req instanceof Request ? req : createRequestFromNodeRequest(req), opts); - } - render(req: NodeIncomingMessage | Request, routeData?: RouteData, locals?: object) { +function makeRequestBody(req: NodeIncomingMessage): BodyProps { + if (req.body !== undefined) { if (typeof req.body === 'string' && req.body.length > 0) { - return super.render( - req instanceof Request ? req : createRequestFromNodeRequest(req, Buffer.from(req.body)), - routeData, - locals - ); + return { body: Buffer.from(req.body) }; } if (typeof req.body === 'object' && req.body !== null && Object.keys(req.body).length > 0) { - return super.render( - req instanceof Request - ? req - : createRequestFromNodeRequest(req, Buffer.from(JSON.stringify(req.body))), - routeData, - locals - ); + return { body: Buffer.from(JSON.stringify(req.body)) }; } - if ('on' in req) { - let body = Buffer.from([]); - let reqBodyComplete = new Promise((resolve, reject) => { - req.on('data', (d) => { - body = Buffer.concat([body, d]); - }); - req.on('end', () => { - resolve(body); - }); - req.on('error', (err) => { - reject(err); - }); - }); + // This covers all async iterables including Readable and ReadableStream. + if ( + typeof req.body === 'object' && + req.body !== null && + typeof (req.body as any)[Symbol.asyncIterator] !== 'undefined' + ) { + return asyncIterableToBodyProps(req.body as AsyncIterable<any>); + } + } + + // Return default body. + return asyncIterableToBodyProps(req); +} + +function asyncIterableToBodyProps(iterable: AsyncIterable<any>): BodyProps { + return { + // Node uses undici for the Request implementation. Undici accepts + // a non-standard async iterable for the body. + // @ts-expect-error + body: iterable, + // The duplex property is required when using a ReadableStream or async + // iterable for the body. The type definitions do not include the duplex + // property because they are not up-to-date. + // @ts-expect-error + duplex: 'half', + } satisfies BodyProps; +} + +class NodeIncomingMessage extends IncomingMessage { + /** + * Allow the request body to be explicitly overridden. For example, this + * is used by the Express JSON middleware. + */ + body?: unknown; +} - return reqBodyComplete.then(() => { - return super.render( - req instanceof Request ? req : createRequestFromNodeRequest(req, body), - routeData, - locals - ); +export class NodeApp extends App { + match(req: NodeIncomingMessage | Request, opts: MatchOptions = {}) { + if (!(req instanceof Request)) { + req = createRequestFromNodeRequest(req, { + emptyBody: true, }); } - return super.render( - req instanceof Request ? req : createRequestFromNodeRequest(req), - routeData, - locals - ); + return super.match(req, opts); + } + render(req: NodeIncomingMessage | Request, routeData?: RouteData, locals?: object) { + if (!(req instanceof Request)) { + req = createRequestFromNodeRequest(req); + } + return super.render(req, routeData, locals); } } diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 00be46ea9..4e89dfb61 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -19,7 +19,7 @@ import type { import { generateImage as generateImageInternal, getStaticImageList, -} from '../../assets/generate.js'; +} from '../../assets/build/generate.js'; import { hasPrerenderedPages, type BuildInternals } from '../../core/build/internal.js'; import { isRelativePath, diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index cbb259e03..a1c7c3e56 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -311,8 +311,12 @@ async function runPostBuildHooks( async function cleanStaticOutput(opts: StaticBuildOptions, internals: BuildInternals) { const allStaticFiles = new Set(); for (const pageData of eachPageData(internals)) { - if (pageData.route.prerender) - allStaticFiles.add(internals.pageToBundleMap.get(pageData.moduleSpecifier)); + if (pageData.route.prerender) { + const { moduleSpecifier } = pageData; + const pageBundleId = internals.pageToBundleMap.get(moduleSpecifier); + const entryBundleId = internals.entrySpecifierToBundleMap.get(moduleSpecifier); + allStaticFiles.add(pageBundleId ?? entryBundleId); + } } const ssr = isServerLikeOutput(opts.settings.config); const out = ssr @@ -340,7 +344,8 @@ async function cleanStaticOutput(opts: StaticBuildOptions, internals: BuildInter // Replace exports (only prerendered pages) with a noop let value = 'const noop = () => {};'; for (const e of exports) { - value += `\nexport const ${e.n} = noop;`; + if (e.n === 'default') value += `\n export default noop;`; + else value += `\nexport const ${e.n} = noop;`; } await fs.promises.writeFile(url, value, { encoding: 'utf8' }); }) @@ -355,6 +360,8 @@ async function cleanServerOutput(opts: StaticBuildOptions) { // The SSR output is all .mjs files, the client output is not. const files = await glob('**/*.mjs', { cwd: fileURLToPath(out), + // Important! Also cleanup dotfiles like `node_modules/.pnpm/**` + dot: true, }); if (files.length) { // Remove all the SSR generated .mjs files diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 48b0f3a59..bff55b392 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -189,6 +189,30 @@ export const AstroConfigSchema = z.object({ ]), config: z.record(z.any()).default({}), }), + domains: z.array(z.string()).default([]), + remotePatterns: z + .array( + z.object({ + protocol: z.string().optional(), + hostname: z + .string() + .refine( + (val) => !val.includes('*') || val.startsWith('*.') || val.startsWith('**.'), + { + message: 'wildcards can only be placed at the beginning of the hostname', + } + ) + .optional(), + port: z.string().optional(), + pathname: z + .string() + .refine((val) => !val.includes('*') || val.endsWith('/*') || val.endsWith('/**'), { + message: 'wildcards can only be placed at the end of a pathname', + }) + .optional(), + }) + ) + .default([]), }) .default({ service: { entrypoint: 'astro/assets/services/sharp', config: {} }, diff --git a/packages/astro/src/core/config/tsconfig.ts b/packages/astro/src/core/config/tsconfig.ts index 5a5d3fc64..a0c78f08c 100644 --- a/packages/astro/src/core/config/tsconfig.ts +++ b/packages/astro/src/core/config/tsconfig.ts @@ -1,4 +1,3 @@ -import { deepmerge } from 'deepmerge-ts'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import * as tsr from 'tsconfig-resolver'; @@ -96,5 +95,28 @@ export function updateTSConfigForFramework( return target; } - return deepmerge(target, presets.get(framework)!); + return deepMergeObjects(target, presets.get(framework)!); +} + +// Simple deep merge implementation that merges objects and strings +function deepMergeObjects<T extends Record<string, any>>(a: T, b: T): T { + const merged: T = { ...a }; + + for (const key in b) { + const value = b[key]; + + if (a[key] == null) { + merged[key] = value; + continue; + } + + if (typeof a[key] === 'object' && typeof value === 'object') { + merged[key] = deepMergeObjects(a[key], value); + continue; + } + + merged[key] = value; + } + + return merged; } diff --git a/packages/astro/src/vite-plugin-integrations-container/index.ts b/packages/astro/src/vite-plugin-integrations-container/index.ts index d6bfd76d7..6cc2da152 100644 --- a/packages/astro/src/vite-plugin-integrations-container/index.ts +++ b/packages/astro/src/vite-plugin-integrations-container/index.ts @@ -16,9 +16,9 @@ export default function astroIntegrationsContainerPlugin({ }): VitePlugin { return { name: 'astro:integration-container', - configureServer(server) { + async configureServer(server) { if (server.config.isProduction) return; - runHookServerSetup({ config: settings.config, server, logging }); + await runHookServerSetup({ config: settings.config, server, logging }); }, async buildStart() { if (settings.injectedRoutes.length === settings.resolvedInjectedRoutes.length) return; diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js index ff0adde52..26fafe41a 100644 --- a/packages/astro/test/core-image.test.js +++ b/packages/astro/test/core-image.test.js @@ -22,6 +22,7 @@ describe('astro:image', () => { root: './fixtures/core-image/', image: { service: testImageService({ foo: 'bar' }), + domains: ['avatars.githubusercontent.com'], }, }); @@ -195,6 +196,15 @@ describe('astro:image', () => { $ = cheerio.load(html); }); + it('has proper link and works', async () => { + let $img = $('#remote img'); + + let src = $img.attr('src'); + expect(src.startsWith('/_image?')).to.be.true; + const imageRequest = await fixture.fetch(src); + expect(imageRequest.status).to.equal(200); + }); + it('includes the provided alt', async () => { let $img = $('#remote img'); expect($img.attr('alt')).to.equal('fred'); @@ -572,6 +582,7 @@ describe('astro:image', () => { root: './fixtures/core-image-ssg/', image: { service: testImageService(), + domains: ['astro.build'], }, }); // Remove cache directory @@ -589,6 +600,15 @@ describe('astro:image', () => { expect(data).to.be.an.instanceOf(Buffer); }); + it('writes out allowed remote images', async () => { + const html = await fixture.readFile('/remote/index.html'); + const $ = cheerio.load(html); + const src = $('#remote img').attr('src'); + expect(src.length).to.be.greaterThan(0); + const data = await fixture.readFile(src, null); + expect(data).to.be.an.instanceOf(Buffer); + }); + it('writes out images to dist folder with proper extension if no format was passed', async () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); @@ -693,12 +713,15 @@ describe('astro:image', () => { }); it('has cache entries', async () => { - const generatedImages = (await fixture.glob('_astro/**/*.webp')).map((path) => - basename(path) - ); - const cachedImages = (await fixture.glob('../node_modules/.astro/assets/**/*.webp')).map( - (path) => basename(path) - ); + const generatedImages = (await fixture.glob('_astro/**/*.webp')) + .map((path) => basename(path)) + .sort(); + const cachedImages = [ + ...(await fixture.glob('../node_modules/.astro/assets/**/*.webp')), + ...(await fixture.glob('../node_modules/.astro/assets/**/*.json')), + ] + .map((path) => basename(path).replace('.webp.json', '.webp')) + .sort(); expect(generatedImages).to.deep.equal(cachedImages); }); diff --git a/packages/astro/test/css-inline-stylesheets.test.js b/packages/astro/test/css-inline-stylesheets.test.js index 63148bbfd..bcd895a47 100644 --- a/packages/astro/test/css-inline-stylesheets.test.js +++ b/packages/astro/test/css-inline-stylesheets.test.js @@ -8,7 +8,11 @@ describe('Setting inlineStylesheets to never in static output', () => { before(async () => { fixture = await loadFixture({ - root: './fixtures/css-inline-stylesheets/never/', + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.dev/', + root: './fixtures/css-inline-stylesheets/', output: 'static', build: { inlineStylesheets: 'never', @@ -41,7 +45,11 @@ describe('Setting inlineStylesheets to never in server output', () => { before(async () => { const fixture = await loadFixture({ - root: './fixtures/css-inline-stylesheets/never/', + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.dev/', + root: './fixtures/css-inline-stylesheets/', output: 'server', adapter: testAdapter(), build: { @@ -77,7 +85,11 @@ describe('Setting inlineStylesheets to auto in static output', () => { before(async () => { fixture = await loadFixture({ - root: './fixtures/css-inline-stylesheets/auto/', + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.info/', + root: './fixtures/css-inline-stylesheets/', output: 'static', build: { inlineStylesheets: 'auto', @@ -117,7 +129,11 @@ describe('Setting inlineStylesheets to auto in server output', () => { before(async () => { const fixture = await loadFixture({ - root: './fixtures/css-inline-stylesheets/auto/', + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.info/', + root: './fixtures/css-inline-stylesheets/', output: 'server', adapter: testAdapter(), build: { @@ -161,7 +177,11 @@ describe('Setting inlineStylesheets to always in static output', () => { before(async () => { fixture = await loadFixture({ - root: './fixtures/css-inline-stylesheets/always/', + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.net/', + root: './fixtures/css-inline-stylesheets/', output: 'static', build: { inlineStylesheets: 'always', @@ -193,7 +213,11 @@ describe('Setting inlineStylesheets to always in server output', () => { before(async () => { const fixture = await loadFixture({ - root: './fixtures/css-inline-stylesheets/always/', + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.net/', + root: './fixtures/css-inline-stylesheets/', output: 'server', adapter: testAdapter(), build: { diff --git a/packages/astro/test/fixtures/core-image-ssg/src/pages/remote.astro b/packages/astro/test/fixtures/core-image-ssg/src/pages/remote.astro new file mode 100644 index 000000000..727a15ff0 --- /dev/null +++ b/packages/astro/test/fixtures/core-image-ssg/src/pages/remote.astro @@ -0,0 +1,7 @@ +--- +import { Image } from "astro:assets"; +--- + +<div id="remote"> +<Image src="https://astro.build/sponsors.png" alt="fred" width="48" height="48" /> +</div> diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/auto/package.json b/packages/astro/test/fixtures/css-inline-stylesheets/auto/package.json deleted file mode 100644 index 3eb8e9d51..000000000 --- a/packages/astro/test/fixtures/css-inline-stylesheets/auto/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/css-inline-stylesheets-auto", - "version": "0.0.0", - "private": true, - "dependencies": { - "astro": "workspace:*" - } -} diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/components/Button.astro b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/components/Button.astro deleted file mode 100644 index 3f25cbd3e..000000000 --- a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/components/Button.astro +++ /dev/null @@ -1,86 +0,0 @@ ---- -const { class: className = '', style, href } = Astro.props; -const { variant = 'primary' } = Astro.props; ---- - -<span class:list={[`link pixel variant-${variant}`, className]} > - <a {href}> - <span><slot /></span> - </a> -</span> - -<style> - .link { - --border-radius: 8; - --duration: 200ms; - --delay: 30ms; - --background: linear-gradient(180deg, var(--link-color-stop-a), var(--link-color-stop-b)); - display: flex; - color: white; - font-size: 1.25rem; - width: max-content; - } - a { - display: flex; - align-items: center; - justify-content: center; - padding: 0.67rem 1.25rem; - width: 100%; - height: 100%; - text-decoration: none; - color: inherit !important; - /* Indicates the button boundaries for forced colors users in older browsers */ - outline: 1px solid transparent; - } - - @media (forced-colors: active) { - a { - border: 1px solid LinkText; - } - } - - a > :global(* + *) { - margin-inline-start: 0.25rem; - } - - .variant-primary { - --variant: primary; - --background: linear-gradient(180deg, var(--link-color-stop-a), var(--link-color-stop-b)); - } - .variant-primary:hover, - .variant-primary:focus-within { - --link-color-stop-a: #6d39ff; - --link-color-stop-b: #af43ff; - } - .variant-primary:active { - --link-color-stop-a: #5f31e1; - --link-color-stop-b: #a740f3; - } - - .variant-outline { - --variant: outline; - --background: none; - color: var(--background); - } - .variant-outline > a::before { - position: absolute; - top: 0; - right: calc(var(--pixel-size) * 1px); - bottom: calc(var(--pixel-size) * 1px); - left: calc(var(--pixel-size) * 1px); - content: ''; - display: block; - transform-origin: bottom center; - background: linear-gradient(to top, var(--background), rgba(255, 255, 255, 0)); - opacity: 0.3; - transform: scaleY(0); - transition: transform 200ms cubic-bezier(0.22, 1, 0.36, 1); - } - .variant-outline:hover > a::before, - .variant-outline:focus-within > a::before { - transform: scaleY(1); - } - .variant-outline:active > a::before { - transform: scaleY(1); - } -</style>
\ No newline at end of file diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/content/en/endeavour.md b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/content/en/endeavour.md deleted file mode 100644 index 240eeeae3..000000000 --- a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/content/en/endeavour.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Endeavour -description: 'Learn about the Endeavour NASA space shuttle.' -publishedDate: 'Sun Jul 11 2021 00:00:00 GMT-0400 (Eastern Daylight Time)' -layout: '../../layouts/Layout.astro' -tags: [space, 90s] ---- - -**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour) - -Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly. - -The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986. - -NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly.
\ No newline at end of file diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/imported.css b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/imported.css deleted file mode 100644 index 3959523ff..000000000 --- a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/imported.css +++ /dev/null @@ -1,15 +0,0 @@ -.bg-skyblue { - background: skyblue; -} - -.bg-lightcoral { - background: lightcoral; -} - -.red { - color: darkred; -} - -.blue { - color: royalblue; -} diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/layouts/Layout.astro b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/layouts/Layout.astro deleted file mode 100644 index 0a2665518..000000000 --- a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/layouts/Layout.astro +++ /dev/null @@ -1,35 +0,0 @@ ---- -import Button from '../components/Button.astro'; -import '../imported.css'; - -interface Props { - title: string; -} - -const { title } = Astro.props; ---- - -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width" /> - <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> - <meta name="generator" content={Astro.generator} /> - <title>{title}</title> - </head> - <body> - <Button>Button used in layout</Button> - <slot /> - </body> -</html> -<style is:global> - html { - font-family: system-ui, sans-serif; - background-color: #F6F6F6; - } - code { - font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, - Bitstream Vera Sans Mono, Courier New, monospace; - } -</style> diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/pages/index.astro b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/pages/index.astro deleted file mode 100644 index bfdbeb5f8..000000000 --- a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/pages/index.astro +++ /dev/null @@ -1,17 +0,0 @@ ---- -import Button from '../components/Button.astro'; -import { getEntryBySlug } from 'astro:content'; - -const entry = await getEntryBySlug('en', 'endeavour'); -const { Content } = await entry.render(); ---- -<style> - #welcome::after { - content: '🚀' - } -</style> -<main> - <h1 id="welcome">Welcome to Astro</h1> - <Content/> - <Button>Button used directly in page</Button> -</main> diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/never/package.json b/packages/astro/test/fixtures/css-inline-stylesheets/never/package.json deleted file mode 100644 index 382288fbc..000000000 --- a/packages/astro/test/fixtures/css-inline-stylesheets/never/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/css-inline-stylesheets-never", - "version": "0.0.0", - "private": true, - "dependencies": { - "astro": "workspace:*" - } -} diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/components/Button.astro b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/components/Button.astro deleted file mode 100644 index 3f25cbd3e..000000000 --- a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/components/Button.astro +++ /dev/null @@ -1,86 +0,0 @@ ---- -const { class: className = '', style, href } = Astro.props; -const { variant = 'primary' } = Astro.props; ---- - -<span class:list={[`link pixel variant-${variant}`, className]} > - <a {href}> - <span><slot /></span> - </a> -</span> - -<style> - .link { - --border-radius: 8; - --duration: 200ms; - --delay: 30ms; - --background: linear-gradient(180deg, var(--link-color-stop-a), var(--link-color-stop-b)); - display: flex; - color: white; - font-size: 1.25rem; - width: max-content; - } - a { - display: flex; - align-items: center; - justify-content: center; - padding: 0.67rem 1.25rem; - width: 100%; - height: 100%; - text-decoration: none; - color: inherit !important; - /* Indicates the button boundaries for forced colors users in older browsers */ - outline: 1px solid transparent; - } - - @media (forced-colors: active) { - a { - border: 1px solid LinkText; - } - } - - a > :global(* + *) { - margin-inline-start: 0.25rem; - } - - .variant-primary { - --variant: primary; - --background: linear-gradient(180deg, var(--link-color-stop-a), var(--link-color-stop-b)); - } - .variant-primary:hover, - .variant-primary:focus-within { - --link-color-stop-a: #6d39ff; - --link-color-stop-b: #af43ff; - } - .variant-primary:active { - --link-color-stop-a: #5f31e1; - --link-color-stop-b: #a740f3; - } - - .variant-outline { - --variant: outline; - --background: none; - color: var(--background); - } - .variant-outline > a::before { - position: absolute; - top: 0; - right: calc(var(--pixel-size) * 1px); - bottom: calc(var(--pixel-size) * 1px); - left: calc(var(--pixel-size) * 1px); - content: ''; - display: block; - transform-origin: bottom center; - background: linear-gradient(to top, var(--background), rgba(255, 255, 255, 0)); - opacity: 0.3; - transform: scaleY(0); - transition: transform 200ms cubic-bezier(0.22, 1, 0.36, 1); - } - .variant-outline:hover > a::before, - .variant-outline:focus-within > a::before { - transform: scaleY(1); - } - .variant-outline:active > a::before { - transform: scaleY(1); - } -</style>
\ No newline at end of file diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/content/en/endeavour.md b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/content/en/endeavour.md deleted file mode 100644 index 240eeeae3..000000000 --- a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/content/en/endeavour.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Endeavour -description: 'Learn about the Endeavour NASA space shuttle.' -publishedDate: 'Sun Jul 11 2021 00:00:00 GMT-0400 (Eastern Daylight Time)' -layout: '../../layouts/Layout.astro' -tags: [space, 90s] ---- - -**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour) - -Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly. - -The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986. - -NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly.
\ No newline at end of file diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/imported.css b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/imported.css deleted file mode 100644 index 3959523ff..000000000 --- a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/imported.css +++ /dev/null @@ -1,15 +0,0 @@ -.bg-skyblue { - background: skyblue; -} - -.bg-lightcoral { - background: lightcoral; -} - -.red { - color: darkred; -} - -.blue { - color: royalblue; -} diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/layouts/Layout.astro b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/layouts/Layout.astro deleted file mode 100644 index 0a2665518..000000000 --- a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/layouts/Layout.astro +++ /dev/null @@ -1,35 +0,0 @@ ---- -import Button from '../components/Button.astro'; -import '../imported.css'; - -interface Props { - title: string; -} - -const { title } = Astro.props; ---- - -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width" /> - <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> - <meta name="generator" content={Astro.generator} /> - <title>{title}</title> - </head> - <body> - <Button>Button used in layout</Button> - <slot /> - </body> -</html> -<style is:global> - html { - font-family: system-ui, sans-serif; - background-color: #F6F6F6; - } - code { - font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, - Bitstream Vera Sans Mono, Courier New, monospace; - } -</style> diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/pages/index.astro b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/pages/index.astro deleted file mode 100644 index bfdbeb5f8..000000000 --- a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/pages/index.astro +++ /dev/null @@ -1,17 +0,0 @@ ---- -import Button from '../components/Button.astro'; -import { getEntryBySlug } from 'astro:content'; - -const entry = await getEntryBySlug('en', 'endeavour'); -const { Content } = await entry.render(); ---- -<style> - #welcome::after { - content: '🚀' - } -</style> -<main> - <h1 id="welcome">Welcome to Astro</h1> - <Content/> - <Button>Button used directly in page</Button> -</main> diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/always/package.json b/packages/astro/test/fixtures/css-inline-stylesheets/package.json index 0d4a8617d..0d4a8617d 100644 --- a/packages/astro/test/fixtures/css-inline-stylesheets/always/package.json +++ b/packages/astro/test/fixtures/css-inline-stylesheets/package.json diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/components/Button.astro b/packages/astro/test/fixtures/css-inline-stylesheets/src/components/Button.astro index 3f25cbd3e..3f25cbd3e 100644 --- a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/components/Button.astro +++ b/packages/astro/test/fixtures/css-inline-stylesheets/src/components/Button.astro diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/content/en/endeavour.md b/packages/astro/test/fixtures/css-inline-stylesheets/src/content/en/endeavour.md index 240eeeae3..240eeeae3 100644 --- a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/content/en/endeavour.md +++ b/packages/astro/test/fixtures/css-inline-stylesheets/src/content/en/endeavour.md diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/imported.css b/packages/astro/test/fixtures/css-inline-stylesheets/src/imported.css index 3959523ff..3959523ff 100644 --- a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/imported.css +++ b/packages/astro/test/fixtures/css-inline-stylesheets/src/imported.css diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/layouts/Layout.astro b/packages/astro/test/fixtures/css-inline-stylesheets/src/layouts/Layout.astro index 0a2665518..0a2665518 100644 --- a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/layouts/Layout.astro +++ b/packages/astro/test/fixtures/css-inline-stylesheets/src/layouts/Layout.astro diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/pages/index.astro b/packages/astro/test/fixtures/css-inline-stylesheets/src/pages/index.astro index bfdbeb5f8..bfdbeb5f8 100644 --- a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/pages/index.astro +++ b/packages/astro/test/fixtures/css-inline-stylesheets/src/pages/index.astro diff --git a/packages/astro/test/fixtures/integration-server-setup/integration.js b/packages/astro/test/fixtures/integration-server-setup/integration.js index bcbd86228..f5000fa6b 100644 --- a/packages/astro/test/fixtures/integration-server-setup/integration.js +++ b/packages/astro/test/fixtures/integration-server-setup/integration.js @@ -1,8 +1,12 @@ +import { setTimeout } from "node:timers/promises"; + export default function() { return { name: '@astrojs/test-integration', hooks: { - 'astro:server:setup': ({ server }) => { + 'astro:server:setup': async ({ server }) => { + // Ensure that `async` is respected + await setTimeout(100); server.middlewares.use( function middleware(req, res, next) { res.setHeader('x-middleware', 'true'); diff --git a/packages/astro/test/fixtures/react-component/astro.config.mjs b/packages/astro/test/fixtures/react-component/astro.config.mjs deleted file mode 100644 index 53d0bd03b..000000000 --- a/packages/astro/test/fixtures/react-component/astro.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from 'astro/config'; -import react from '@astrojs/react'; -import vue from '@astrojs/vue'; - -// https://astro.build/config -export default defineConfig({ - integrations: [react(), vue()], -});
\ No newline at end of file diff --git a/packages/astro/test/fixtures/react-component/package.json b/packages/astro/test/fixtures/react-component/package.json deleted file mode 100644 index cf7b2b057..000000000 --- a/packages/astro/test/fixtures/react-component/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "@test/react-component", - "version": "0.0.0", - "private": true, - "dependencies": { - "@astrojs/react": "workspace:*", - "@astrojs/vue": "workspace:*", - "astro": "workspace:*", - "react": "^18.1.0", - "react-dom": "^18.1.0", - "vue": "^3.3.4" - } -} diff --git a/packages/astro/test/fixtures/react-component/src/components/ArrowFunction.jsx b/packages/astro/test/fixtures/react-component/src/components/ArrowFunction.jsx deleted file mode 100644 index 16fac5bb6..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/ArrowFunction.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default () => { - return <div id="arrow-fn-component"></div>; -} diff --git a/packages/astro/test/fixtures/react-component/src/components/CloneElement.jsx b/packages/astro/test/fixtures/react-component/src/components/CloneElement.jsx deleted file mode 100644 index 809ac4aa4..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/CloneElement.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import { cloneElement } from 'react'; - -const ClonedWithProps = (element) => (props) => - cloneElement(element, props); - -export default ClonedWithProps(<div id="cloned">Cloned With Props</div>); diff --git a/packages/astro/test/fixtures/react-component/src/components/ForgotImport.jsx b/packages/astro/test/fixtures/react-component/src/components/ForgotImport.jsx deleted file mode 100644 index 9ee27faca..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/ForgotImport.jsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function ({}) { - return <h2>oops</h2>; -} diff --git a/packages/astro/test/fixtures/react-component/src/components/GetSearch.jsx b/packages/astro/test/fixtures/react-component/src/components/GetSearch.jsx deleted file mode 100644 index d3fee2f9a..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/GetSearch.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -function GetSearch() { - return (<div>{window.location.search}</div>); -} - -export default GetSearch diff --git a/packages/astro/test/fixtures/react-component/src/components/Goodbye.vue b/packages/astro/test/fixtures/react-component/src/components/Goodbye.vue deleted file mode 100644 index 430dfdb71..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/Goodbye.vue +++ /dev/null @@ -1,11 +0,0 @@ -<template> - <h2 id="vue-h2">Hasta la vista, {{ name }}</h2> -</template> - -<script> -export default { - props: { - name: String, - }, -}; -</script> diff --git a/packages/astro/test/fixtures/react-component/src/components/Hello.jsx b/packages/astro/test/fixtures/react-component/src/components/Hello.jsx deleted file mode 100644 index 4c241162d..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/Hello.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default function ({ name, unused }) { - return <h2 id={`react-${name}`}>Hello {name}!</h2>; -} diff --git a/packages/astro/test/fixtures/react-component/src/components/ImportsThrowsAnError.jsx b/packages/astro/test/fixtures/react-component/src/components/ImportsThrowsAnError.jsx deleted file mode 100644 index d6ff21dc3..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/ImportsThrowsAnError.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import ThrowsAnError from "./ThrowsAnError"; - -export default function() { - return <> - <ThrowsAnError /> - </> -} diff --git a/packages/astro/test/fixtures/react-component/src/components/LazyComponent.jsx b/packages/astro/test/fixtures/react-component/src/components/LazyComponent.jsx deleted file mode 100644 index b43aa36be..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/LazyComponent.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -export const LazyComponent = () => { - return ( - <span id="lazy">inner content</span> - ); -}; - -export default LazyComponent; diff --git a/packages/astro/test/fixtures/react-component/src/components/PragmaComment.jsx b/packages/astro/test/fixtures/react-component/src/components/PragmaComment.jsx deleted file mode 100644 index d8ea77810..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/PragmaComment.jsx +++ /dev/null @@ -1,5 +0,0 @@ -/** @jsxImportSource react */ - -export default function() { - return <div className="pragma-comment">Hello world</div>; -} diff --git a/packages/astro/test/fixtures/react-component/src/components/PragmaCommentTypeScript.tsx b/packages/astro/test/fixtures/react-component/src/components/PragmaCommentTypeScript.tsx deleted file mode 100644 index 9f2256fbf..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/PragmaCommentTypeScript.tsx +++ /dev/null @@ -1,5 +0,0 @@ -/** @jsxImportSource react */ - -export default function({}: object) { - return <div className="pragma-comment">Hello world</div>; -} diff --git a/packages/astro/test/fixtures/react-component/src/components/PropsSpread.jsx b/packages/astro/test/fixtures/react-component/src/components/PropsSpread.jsx deleted file mode 100644 index 044c2a019..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/PropsSpread.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default (props) => { - return <div id="component-spread-props">{props.text}</div>; -} diff --git a/packages/astro/test/fixtures/react-component/src/components/Pure.jsx b/packages/astro/test/fixtures/react-component/src/components/Pure.jsx deleted file mode 100644 index 6fae8613b..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/Pure.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; - -export default class StaticComponent extends React.PureComponent { - - render() { - return ( - <div id="pure"> - <h1>Static component</h1> - </div> - ) - } - -}
\ No newline at end of file diff --git a/packages/astro/test/fixtures/react-component/src/components/Research.jsx b/packages/astro/test/fixtures/react-component/src/components/Research.jsx deleted file mode 100644 index 9ab83e5f3..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/Research.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import * as React from 'react' - -export function Research2() { - const [value] = React.useState(1) - - return <div id="research">foo bar {value}</div> -}
\ No newline at end of file diff --git a/packages/astro/test/fixtures/react-component/src/components/Suspense.jsx b/packages/astro/test/fixtures/react-component/src/components/Suspense.jsx deleted file mode 100644 index 87dc82625..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/Suspense.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React, { Suspense } from 'react'; -const LazyComponent = React.lazy(() => import('./LazyComponent.jsx')); - -export const ParentComponent = () => { - return ( - <div id="outer"> - <Suspense> - <LazyComponent /> - </Suspense> - </div> - ); -}; - -export default ParentComponent; diff --git a/packages/astro/test/fixtures/react-component/src/components/ThrowsAnError.jsx b/packages/astro/test/fixtures/react-component/src/components/ThrowsAnError.jsx deleted file mode 100644 index cf970e38c..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/ThrowsAnError.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useState } from 'react'; - -export default function() { - let player = undefined; - // This is tested in dev mode, so make it work during the build to prevent - // breaking other tests. - if(import.meta.env.MODE === 'production') { - player = {}; - } - const [] = useState(player.currentTime || null); - - return ( - <div>Should have thrown</div> - ) -} diff --git a/packages/astro/test/fixtures/react-component/src/components/TypeScriptComponent.tsx b/packages/astro/test/fixtures/react-component/src/components/TypeScriptComponent.tsx deleted file mode 100644 index bde96da84..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/TypeScriptComponent.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default function({}) { - return <div className="ts-component">Hello world</div>; -} diff --git a/packages/astro/test/fixtures/react-component/src/components/WithChildren.jsx b/packages/astro/test/fixtures/react-component/src/components/WithChildren.jsx deleted file mode 100644 index cdcb0e0a6..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/WithChildren.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default function ({ children }) { - return <div className="with-children">{children}</div>; -} diff --git a/packages/astro/test/fixtures/react-component/src/components/WithId.jsx b/packages/astro/test/fixtures/react-component/src/components/WithId.jsx deleted file mode 100644 index 0abe91c72..000000000 --- a/packages/astro/test/fixtures/react-component/src/components/WithId.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; - -export default function () { - const id = React.useId(); - return <p className='react-use-id' id={id}>{id}</p>; -} diff --git a/packages/astro/test/fixtures/react-component/src/pages/error-rendering.astro b/packages/astro/test/fixtures/react-component/src/pages/error-rendering.astro deleted file mode 100644 index 6984a6da5..000000000 --- a/packages/astro/test/fixtures/react-component/src/pages/error-rendering.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- -import ImportsThrowsAnError from '../components/ImportsThrowsAnError'; ---- -<html> -<head> - <title>Testing</title> -</head> -<body> - <ImportsThrowsAnError /> -</body> -</html> diff --git a/packages/astro/test/fixtures/react-component/src/pages/index.astro b/packages/astro/test/fixtures/react-component/src/pages/index.astro deleted file mode 100644 index 3afd8233f..000000000 --- a/packages/astro/test/fixtures/react-component/src/pages/index.astro +++ /dev/null @@ -1,41 +0,0 @@ ---- -import Hello from '../components/Hello.jsx'; -import Later from '../components/Goodbye.vue'; -import ArrowFunction from '../components/ArrowFunction.jsx'; -import PropsSpread from '../components/PropsSpread.jsx'; -import {Research2} from '../components/Research.jsx'; -import Pure from '../components/Pure.jsx'; -import TypeScriptComponent from '../components/TypeScriptComponent'; -import CloneElement from '../components/CloneElement'; -import WithChildren from '../components/WithChildren'; -import WithId from '../components/WithId'; - -const someProps = { - text: 'Hello world!', -}; ---- - -<html> - <head> - <!-- Head Stuff --> - </head> - <body> - <Hello name="static" /> - <Hello name="load" client:load /> - <!-- Test island deduplication, i.e. same UID as the component above. --> - <Hello name="load" client:load /> - <!-- Test island deduplication account for non-render affecting props. --> - <Hello name="load" unused="" client:load /> - <Later name="baby" /> - <ArrowFunction /> - <PropsSpread {...someProps}/> - <Research2 client:idle /> - <TypeScriptComponent client:load /> - <Pure /> - <CloneElement /> - <WithChildren client:load>test</WithChildren> - <WithChildren client:load children="test" /> - <WithId client:idle /> - <WithId client:idle /> - </body> -</html> diff --git a/packages/astro/test/fixtures/react-component/src/pages/pragma-comment.astro b/packages/astro/test/fixtures/react-component/src/pages/pragma-comment.astro deleted file mode 100644 index b3ddba639..000000000 --- a/packages/astro/test/fixtures/react-component/src/pages/pragma-comment.astro +++ /dev/null @@ -1,14 +0,0 @@ ---- -import PragmaComponent from '../components/PragmaComment.jsx'; -import PragmaComponentTypeScript from '../components/PragmaCommentTypeScript.tsx'; ---- - -<html> -<head> - <title>React component works with Pragma comment</title> -</head> -<body> - <PragmaComponent client:load/> - <PragmaComponentTypeScript client:load/> -</body> -</html> diff --git a/packages/astro/test/fixtures/react-component/src/pages/suspense.astro b/packages/astro/test/fixtures/react-component/src/pages/suspense.astro deleted file mode 100644 index 5a9d15644..000000000 --- a/packages/astro/test/fixtures/react-component/src/pages/suspense.astro +++ /dev/null @@ -1,17 +0,0 @@ ---- -import Suspense from '../components/Suspense.jsx'; ---- - -<html> - <head> - <!-- Head Stuff --> - </head> - <body> - <div id="client"> - <Suspense client:load /> - </div> - <div id="server"> - <Suspense /> - </div> - </body> -</html> diff --git a/packages/astro/test/fixtures/react-component/src/skipped-pages/forgot-import.astro b/packages/astro/test/fixtures/react-component/src/skipped-pages/forgot-import.astro deleted file mode 100644 index de5d319d9..000000000 --- a/packages/astro/test/fixtures/react-component/src/skipped-pages/forgot-import.astro +++ /dev/null @@ -1,12 +0,0 @@ ---- -import ForgotImport from '../components/ForgotImport.jsx'; ---- - -<html> -<head> - <title>Here we are</title> -</head> -<body> - <ForgotImport /> -</body> -</html>
\ No newline at end of file diff --git a/packages/astro/test/fixtures/react-component/src/skipped-pages/window.astro b/packages/astro/test/fixtures/react-component/src/skipped-pages/window.astro deleted file mode 100644 index e780f3c44..000000000 --- a/packages/astro/test/fixtures/react-component/src/skipped-pages/window.astro +++ /dev/null @@ -1,8 +0,0 @@ ---- -import GetSearch from '../components/GetSearch.jsx'; ---- -<html> -<body> - <GetSearch /> -</body> -</html> diff --git a/packages/astro/test/react-component.test.js b/packages/astro/test/react-component.test.js deleted file mode 100644 index a6bb8cfae..000000000 --- a/packages/astro/test/react-component.test.js +++ /dev/null @@ -1,165 +0,0 @@ -import { expect } from 'chai'; -import { load as cheerioLoad } from 'cheerio'; -import { isWindows, loadFixture } from './test-utils.js'; - -let fixture; - -describe('React Components', () => { - before(async () => { - fixture = await loadFixture({ - root: './fixtures/react-component/', - }); - }); - - describe('build', () => { - before(async () => { - await fixture.build(); - }); - - it('Can load React', async () => { - const html = await fixture.readFile('/index.html'); - const $ = cheerioLoad(html); - - // test 1: basic component renders - expect($('#react-static').text()).to.equal('Hello static!'); - - // test 2: no reactroot - expect($('#react-static').attr('data-reactroot')).to.equal(undefined); - - // test 3: Can use function components - expect($('#arrow-fn-component')).to.have.lengthOf(1); - - // test 4: Can use spread for components - expect($('#component-spread-props')).to.have.lengthOf(1); - - // test 5: spread props renders - expect($('#component-spread-props').text(), 'Hello world!'); - - // test 6: Can use TS components - expect($('.ts-component')).to.have.lengthOf(1); - - // test 7: Can use Pure components - expect($('#pure')).to.have.lengthOf(1); - - // test 8: Check number of islands - expect($('astro-island[uid]')).to.have.lengthOf(9); - - // test 9: Check island deduplication - const uniqueRootUIDs = new Set($('astro-island').map((i, el) => $(el).attr('uid'))); - expect(uniqueRootUIDs.size).to.equal(8); - - // test 10: Should properly render children passed as props - const islandsWithChildren = $('.with-children'); - expect(islandsWithChildren).to.have.lengthOf(2); - expect($(islandsWithChildren[0]).html()).to.equal($(islandsWithChildren[1]).html()); - - // test 11: Should generate unique React.useId per island - const islandsWithId = $('.react-use-id'); - expect(islandsWithId).to.have.lengthOf(2); - expect($(islandsWithId[0]).attr('id')).to.not.equal($(islandsWithId[1]).attr('id')); - }); - - it('Can load Vue', async () => { - const html = await fixture.readFile('/index.html'); - const $ = cheerioLoad(html); - expect($('#vue-h2').text()).to.equal('Hasta la vista, baby'); - }); - - it('Can use a pragma comment', async () => { - const html = await fixture.readFile('/pragma-comment/index.html'); - const $ = cheerioLoad(html); - - // test 1: rendered the PragmaComment component - expect($('.pragma-comment')).to.have.lengthOf(2); - }); - - // TODO: is this still a relevant test? - it.skip('Includes reactroot on hydrating components', async () => { - const html = await fixture.readFile('/index.html'); - const $ = cheerioLoad(html); - - const div = $('#research'); - - // test 1: has the hydration attr - expect(div.attr('data-reactroot')).to.be.ok; - - // test 2: renders correctly - expect(div.html()).to.equal('foo bar <!-- -->1'); - }); - - it('Can load Suspense-using components', async () => { - const html = await fixture.readFile('/suspense/index.html'); - const $ = cheerioLoad(html); - expect($('#client #lazy')).to.have.lengthOf(1); - expect($('#server #lazy')).to.have.lengthOf(1); - }); - - it('Can pass through props with cloneElement', async () => { - const html = await fixture.readFile('/index.html'); - const $ = cheerioLoad(html); - expect($('#cloned').text()).to.equal('Cloned With Props'); - }); - }); - - if (isWindows) return; - - describe('dev', () => { - /** @type {import('./test-utils').Fixture} */ - let devServer; - - before(async () => { - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('scripts proxy correctly', async () => { - const html = await fixture.fetch('/').then((res) => res.text()); - const $ = cheerioLoad(html); - - for (const script of $('script').toArray()) { - const { src } = script.attribs; - if (!src) continue; - expect((await fixture.fetch(src)).status, `404: ${src}`).to.equal(200); - } - }); - - // TODO: move this to separate dev test? - it.skip('Throws helpful error message on window SSR', async () => { - const html = await fixture.fetch('/window/index.html'); - expect(html).to.include( - `[/window] - The window object is not available during server-side rendering (SSR). - Try using \`import.meta.env.SSR\` to write SSR-friendly code. - https://docs.astro.build/reference/api-reference/#importmeta` - ); - }); - - // In moving over to Vite, the jsx-runtime import is now obscured. TODO: update the method of finding this. - it.skip('uses the new JSX transform', async () => { - const html = await fixture.fetch('/index.html'); - - // Grab the imports - const exp = /import\("(.+?)"\)/g; - let match, componentUrl; - while ((match = exp.exec(html))) { - if (match[1].includes('Research.js')) { - componentUrl = match[1]; - break; - } - } - const component = await fixture.readFile(componentUrl); - const jsxRuntime = component.imports.filter((i) => i.specifier.includes('jsx-runtime')); - - // test 1: react/jsx-runtime is used for the component - expect(jsxRuntime).to.be.ok; - }); - - it('When a nested component throws it does not crash the server', async () => { - const res = await fixture.fetch('/error-rendering'); - await res.arrayBuffer(); - }); - }); -}); diff --git a/packages/astro/test/ssr-prerender.test.js b/packages/astro/test/ssr-prerender.test.js index 90ec1b6fa..567371f0b 100644 --- a/packages/astro/test/ssr-prerender.test.js +++ b/packages/astro/test/ssr-prerender.test.js @@ -30,8 +30,7 @@ describe('SSR: prerender', () => { const app = await fixture.loadTestAdapterApp(); /** @type {Set<string>} */ const assets = app.manifest.assets; - expect(assets.size).to.equal(1); - expect(Array.from(assets)[0].endsWith('static/index.html')).to.be.true; + expect(assets).to.contain('/static/index.html'); }); }); diff --git a/packages/astro/test/test-image-service.js b/packages/astro/test/test-image-service.js index ebdbb0765..bcf623caa 100644 --- a/packages/astro/test/test-image-service.js +++ b/packages/astro/test/test-image-service.js @@ -17,8 +17,8 @@ export default { ...baseService, getHTMLAttributes(options, serviceConfig) { options['data-service'] = 'my-custom-service'; - if (serviceConfig.foo) { - options['data-service-config'] = serviceConfig.foo; + if (serviceConfig.service.config.foo) { + options['data-service-config'] = serviceConfig.service.config.foo; } return baseService.getHTMLAttributes(options); }, diff --git a/packages/astro/test/units/assets/remote-pattern.test.js b/packages/astro/test/units/assets/remote-pattern.test.js new file mode 100644 index 000000000..62a411e3a --- /dev/null +++ b/packages/astro/test/units/assets/remote-pattern.test.js @@ -0,0 +1,111 @@ +import { expect } from 'chai'; +import { + matchProtocol, + matchPort, + matchHostname, + matchPathname, + matchPattern, +} from '../../../dist/assets/utils/remotePattern.js'; + +describe('astro/src/assets/utils/remotePattern', () => { + const url1 = new URL('https://docs.astro.build/en/getting-started'); + const url2 = new URL('http://preview.docs.astro.build:8080/'); + const url3 = new URL('https://astro.build/'); + const url4 = new URL('https://example.co/'); + + describe('remote pattern matchers', () => { + it('matches protocol', async () => { + // undefined + expect(matchProtocol(url1)).to.be.true; + + // defined, true/false + expect(matchProtocol(url1, 'http')).to.be.false; + expect(matchProtocol(url1, 'https')).to.be.true; + }); + + it('matches port', async () => { + // undefined + expect(matchPort(url1)).to.be.true; + + // defined, but port is empty (default port used in URL) + expect(matchPort(url1, '')).to.be.true; + + // defined and port is custom + expect(matchPort(url2, '8080')).to.be.true; + }); + + it('matches hostname (no wildcards)', async () => { + // undefined + expect(matchHostname(url1)).to.be.true; + + // defined, true/false + expect(matchHostname(url1, 'astro.build')).to.be.false; + expect(matchHostname(url1, 'docs.astro.build')).to.be.true; + }); + + it('matches hostname (with wildcards)', async () => { + // defined, true/false + expect(matchHostname(url1, 'docs.astro.build', true)).to.be.true; + expect(matchHostname(url1, '**.astro.build', true)).to.be.true; + expect(matchHostname(url1, '*.astro.build', true)).to.be.true; + + expect(matchHostname(url2, '*.astro.build', true)).to.be.false; + expect(matchHostname(url2, '**.astro.build', true)).to.be.true; + + expect(matchHostname(url3, 'astro.build', true)).to.be.true; + expect(matchHostname(url3, '*.astro.build', true)).to.be.false; + expect(matchHostname(url3, '**.astro.build', true)).to.be.false; + }); + + it('matches pathname (no wildcards)', async () => { + // undefined + expect(matchPathname(url1)).to.be.true; + + // defined, true/false + expect(matchPathname(url1, '/')).to.be.false; + expect(matchPathname(url1, '/en/getting-started')).to.be.true; + }); + + it('matches pathname (with wildcards)', async () => { + // defined, true/false + expect(matchPathname(url1, '/en/**', true)).to.be.true; + expect(matchPathname(url1, '/en/*', true)).to.be.true; + expect(matchPathname(url1, '/**', true)).to.be.true; + + expect(matchPathname(url2, '/**', true)).to.be.false; + expect(matchPathname(url2, '/*', true)).to.be.false; + }); + + it('matches patterns', async () => { + expect(matchPattern(url1, {})).to.be.true; + + expect( + matchPattern(url1, { + protocol: 'https', + }) + ).to.be.true; + + expect( + matchPattern(url1, { + protocol: 'https', + hostname: '**.astro.build', + }) + ).to.be.true; + + expect( + matchPattern(url1, { + protocol: 'https', + hostname: '**.astro.build', + pathname: '/en/**', + }) + ).to.be.true; + + expect( + matchPattern(url4, { + protocol: 'https', + hostname: 'example.com', + }) + ).to.be.false; + }); + }); +}); |