-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: removed imports from @astrojs/vercel/dist
- Loading branch information
Showing
6 changed files
with
361 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/** | ||
* While Vercel adds the `PUBLIC_` prefix for their `VERCEL_` env vars by default, some env vars | ||
* like `VERCEL_ANALYTICS_ID` aren't, so handle them here so that it works correctly in runtime. | ||
*/ | ||
export function exposeEnv(envs: string[]): Record<string, unknown> { | ||
const mapped: Record<string, unknown> = {}; | ||
|
||
envs | ||
.filter((env) => process.env[env]) | ||
.forEach((env) => { | ||
mapped[`import.meta.env.PUBLIC_${env}`] = JSON.stringify(process.env[env]); | ||
}); | ||
|
||
return mapped; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import type { PathLike } from 'node:fs'; | ||
import * as fs from 'node:fs/promises'; | ||
import nodePath from 'node:path'; | ||
import { fileURLToPath } from 'node:url'; | ||
|
||
export async function writeJson<T>(path: PathLike, data: T) { | ||
await fs.writeFile(path, JSON.stringify(data, null, '\t'), { encoding: 'utf-8' }); | ||
} | ||
|
||
export async function removeDir(dir: PathLike) { | ||
await fs.rm(dir, { recursive: true, force: true, maxRetries: 3 }); | ||
} | ||
|
||
export async function emptyDir(dir: PathLike): Promise<void> { | ||
await removeDir(dir); | ||
await fs.mkdir(dir, { recursive: true }); | ||
} | ||
|
||
export async function getFilesFromFolder(dir: URL) { | ||
const data = await fs.readdir(dir, { withFileTypes: true }); | ||
let files: URL[] = []; | ||
for (const item of data) { | ||
if (item.isDirectory()) { | ||
const moreFiles = await getFilesFromFolder(new URL(`./${item.name}/`, dir)); | ||
files = files.concat(moreFiles); | ||
} else { | ||
files.push(new URL(`./${item.name}`, dir)); | ||
} | ||
} | ||
return files; | ||
} | ||
|
||
export const getVercelOutput = (root: URL) => new URL('./.vercel/output/', root); | ||
|
||
/** | ||
* Copies files into a folder keeping the folder structure intact. | ||
* The resulting file tree will start at the common ancestor. | ||
* | ||
* @param {URL[]} files A list of files to copy (absolute path). | ||
* @param {URL} outDir Destination folder where to copy the files to (absolute path). | ||
* @param {URL[]} [exclude] A list of files to exclude (absolute path). | ||
* @returns {Promise<string>} The common ancestor of the copied files. | ||
*/ | ||
export async function copyFilesToFunction( | ||
files: URL[], | ||
outDir: URL, | ||
exclude: URL[] = [] | ||
): Promise<string> { | ||
const excludeList = exclude.map(fileURLToPath); | ||
const fileList = files.map(fileURLToPath).filter((f) => !excludeList.includes(f)); | ||
|
||
if (files.length === 0) throw new Error('[@astrojs/vercel] No files found to copy'); | ||
|
||
let commonAncestor = nodePath.dirname(fileList[0]); | ||
for (const file of fileList.slice(1)) { | ||
while (!file.startsWith(commonAncestor)) { | ||
commonAncestor = nodePath.dirname(commonAncestor); | ||
} | ||
} | ||
|
||
for (const origin of fileList) { | ||
const dest = new URL(nodePath.relative(commonAncestor, origin), outDir); | ||
|
||
const realpath = await fs.realpath(origin); | ||
const isSymlink = realpath !== origin; | ||
const isDir = (await fs.stat(origin)).isDirectory(); | ||
|
||
// Create directories recursively | ||
if (isDir && !isSymlink) { | ||
await fs.mkdir(new URL('..', dest), { recursive: true }); | ||
} else { | ||
await fs.mkdir(new URL('.', dest), { recursive: true }); | ||
} | ||
|
||
if (isSymlink) { | ||
const realdest = fileURLToPath(new URL(nodePath.relative(commonAncestor, realpath), outDir)); | ||
await fs.symlink( | ||
nodePath.relative(fileURLToPath(new URL('.', dest)), realdest), | ||
dest, | ||
isDir ? 'dir' : 'file' | ||
); | ||
} else if (!isDir) { | ||
await fs.copyFile(origin, dest); | ||
} | ||
} | ||
|
||
return commonAncestor; | ||
} | ||
|
||
export async function writeFile(path: PathLike, content: string) { | ||
await fs.writeFile(path, content, { encoding: 'utf-8' }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import type { ImageMetadata, ImageQualityPreset, ImageTransform } from 'astro'; | ||
|
||
export const defaultImageConfig: VercelImageConfig = { | ||
sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], | ||
domains: [], | ||
}; | ||
|
||
export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata { | ||
return typeof src === 'object'; | ||
} | ||
// https://vercel.com/docs/build-output-api/v3/configuration#images | ||
type ImageFormat = 'image/avif' | 'image/webp'; | ||
|
||
type RemotePattern = { | ||
protocol?: 'http' | 'https'; | ||
hostname: string; | ||
port?: string; | ||
pathname?: string; | ||
}; | ||
|
||
export type VercelImageConfig = { | ||
/** | ||
* Supported image widths. | ||
*/ | ||
sizes: number[]; | ||
/** | ||
* Allowed external domains that can use Image Optimization. Leave empty for only allowing the deployment domain to use Image Optimization. | ||
*/ | ||
domains: string[]; | ||
/** | ||
* Allowed external patterns that can use Image Optimization. Similar to `domains` but provides more control with RegExp. | ||
*/ | ||
remotePatterns?: RemotePattern[]; | ||
/** | ||
* Cache duration (in seconds) for the optimized images. | ||
*/ | ||
minimumCacheTTL?: number; | ||
/** | ||
* Supported output image formats | ||
*/ | ||
formats?: ImageFormat[]; | ||
/** | ||
* Allow SVG input image URLs. This is disabled by default for security purposes. | ||
*/ | ||
dangerouslyAllowSVG?: boolean; | ||
/** | ||
* Change the [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) of the optimized images. | ||
*/ | ||
contentSecurityPolicy?: string; | ||
}; | ||
|
||
export const qualityTable: Record<ImageQualityPreset, number> = { | ||
low: 25, | ||
mid: 50, | ||
high: 80, | ||
max: 100, | ||
}; | ||
|
||
export function getImageConfig( | ||
images: boolean | undefined, | ||
imagesConfig: VercelImageConfig | undefined, | ||
command: string | ||
) { | ||
if (images) { | ||
return { | ||
image: { | ||
service: { | ||
entrypoint: | ||
command === 'dev' | ||
? '@astrojs/vercel/dev-image-service' | ||
: '@astrojs/vercel/build-image-service', | ||
config: imagesConfig ? imagesConfig : defaultImageConfig, | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
return {}; | ||
} | ||
|
||
export function sharedValidateOptions( | ||
options: ImageTransform, | ||
serviceConfig: Record<string, any>, | ||
mode: 'development' | 'production' | ||
) { | ||
const vercelImageOptions = serviceConfig as VercelImageConfig; | ||
|
||
if ( | ||
mode === 'development' && | ||
(!vercelImageOptions.sizes || vercelImageOptions.sizes.length === 0) | ||
) { | ||
throw new Error('Vercel Image Optimization requires at least one size to be configured.'); | ||
} | ||
|
||
const configuredWidths = vercelImageOptions.sizes.sort((a, b) => a - b); | ||
|
||
// The logic for finding the perfect width is a bit confusing, here it goes: | ||
// For images where no width has been specified: | ||
// - For local, imported images, fallback to nearest width we can find in our configured | ||
// - For remote images, that's an error, width is always required. | ||
// For images where a width has been specified: | ||
// - If the width that the user asked for isn't in `sizes`, then fallback to the nearest one, but save the width | ||
// the user asked for so we can put it on the `img` tag later. | ||
// - Otherwise, just use as-is. | ||
// The end goal is: | ||
// - The size on the page is always the one the user asked for or the base image's size | ||
// - The actual size of the image file is always one of `sizes`, either the one the user asked for or the nearest to it | ||
if (!options.width) { | ||
const src = options.src; | ||
if (isESMImportedImage(src)) { | ||
const nearestWidth = configuredWidths.reduce((prev, curr) => { | ||
return Math.abs(curr - src.width) < Math.abs(prev - src.width) ? curr : prev; | ||
}); | ||
|
||
// Use the image's base width to inform the `width` and `height` on the `img` tag | ||
options.inputtedWidth = src.width; | ||
options.width = nearestWidth; | ||
} else { | ||
throw new Error(`Missing \`width\` parameter for remote image ${options.src}`); | ||
} | ||
} else { | ||
if (!configuredWidths.includes(options.width)) { | ||
const nearestWidth = configuredWidths.reduce((prev, curr) => { | ||
return Math.abs(curr - options.width!) < Math.abs(prev - options.width!) ? curr : prev; | ||
}); | ||
|
||
// Save the width the user asked for to inform the `width` and `height` on the `img` tag | ||
options.inputtedWidth = options.width; | ||
options.width = nearestWidth; | ||
} | ||
} | ||
|
||
if (options.quality && typeof options.quality === 'string') { | ||
options.quality = options.quality in qualityTable ? qualityTable[options.quality] : undefined; | ||
} | ||
|
||
if (!options.quality) { | ||
options.quality = 100; | ||
} | ||
|
||
return options; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import { appendForwardSlash } from '@astrojs/internal-helpers/path'; | ||
import type { AstroConfig, RouteData, RoutePart } from 'astro'; | ||
import nodePath from 'node:path'; | ||
|
||
const pathJoin = nodePath.posix.join; | ||
|
||
// https://vercel.com/docs/project-configuration#legacy/routes | ||
interface VercelRoute { | ||
src: string; | ||
methods?: string[]; | ||
dest?: string; | ||
headers?: Record<string, string>; | ||
status?: number; | ||
continue?: boolean; | ||
} | ||
|
||
// Copied from /home/juanm04/dev/misc/astro/packages/astro/src/core/routing/manifest/create.ts | ||
// 2022-04-26 | ||
function getMatchPattern(segments: RoutePart[][]) { | ||
return segments | ||
.map((segment) => { | ||
return segment[0].spread | ||
? '(?:\\/(.*?))?' | ||
: '\\/' + | ||
segment | ||
.map((part) => { | ||
if (part) | ||
return part.dynamic | ||
? '([^/]+?)' | ||
: part.content | ||
.normalize() | ||
.replace(/\?/g, '%3F') | ||
.replace(/#/g, '%23') | ||
.replace(/%5B/g, '[') | ||
.replace(/%5D/g, ']') | ||
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||
}) | ||
.join(''); | ||
}) | ||
.join(''); | ||
} | ||
|
||
function getReplacePattern(segments: RoutePart[][]) { | ||
let n = 0; | ||
let result = ''; | ||
|
||
for (const segment of segments) { | ||
for (const part of segment) { | ||
if (part.dynamic) result += '$' + ++n; | ||
else result += part.content; | ||
} | ||
result += '/'; | ||
} | ||
|
||
// Remove trailing slash | ||
result = result.slice(0, -1); | ||
|
||
return result; | ||
} | ||
|
||
function getRedirectLocation(route: RouteData, config: AstroConfig): string { | ||
if (route.redirectRoute) { | ||
const pattern = getReplacePattern(route.redirectRoute.segments); | ||
const path = config.trailingSlash === 'always' ? appendForwardSlash(pattern) : pattern; | ||
return pathJoin(config.base, path); | ||
} else if (typeof route.redirect === 'object') { | ||
return pathJoin(config.base, route.redirect.destination); | ||
} else { | ||
return pathJoin(config.base, route.redirect || ''); | ||
} | ||
} | ||
|
||
function getRedirectStatus(route: RouteData): number { | ||
if (typeof route.redirect === 'object') { | ||
return route.redirect.status; | ||
} | ||
return 301; | ||
} | ||
|
||
export function getRedirects(routes: RouteData[], config: AstroConfig): VercelRoute[] { | ||
let redirects: VercelRoute[] = []; | ||
|
||
for (const route of routes) { | ||
if (route.type === 'redirect') { | ||
redirects.push({ | ||
src: config.base + getMatchPattern(route.segments), | ||
headers: { Location: getRedirectLocation(route, config) }, | ||
status: getRedirectStatus(route), | ||
}); | ||
} else if (route.type === 'page' && route.route !== '/') { | ||
if (config.trailingSlash === 'always') { | ||
redirects.push({ | ||
src: config.base + getMatchPattern(route.segments), | ||
headers: { Location: config.base + getReplacePattern(route.segments) + '/' }, | ||
status: 308, | ||
}); | ||
} else if (config.trailingSlash === 'never') { | ||
redirects.push({ | ||
src: config.base + getMatchPattern(route.segments) + '/', | ||
headers: { Location: config.base + getReplacePattern(route.segments) }, | ||
status: 308, | ||
}); | ||
} | ||
} | ||
} | ||
|
||
return redirects; | ||
} |