-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: [image] Fixing SSR support and improving error validation (#4013)
* fix: SSR builds were hitting an undefined error and skipping the step for copying original assets * chore: update lockfile * chore: adding better error validation to getImage and getPicture * refactor: cleaning up index.ts * refactor: moving SSG build generation logic out of the integration * splitting build to ssg & ssr helpers, re-enabling SSR image build tests * sharp should automatically rotate based on EXIF * cleaning up how static images are tracked for SSG builds * undo unrelated mod.d.ts change * chore: add changeset
- Loading branch information
Tony Sullivan
authored
Jul 22, 2022
1 parent
41f4a8f
commit ef93457
Showing
48 changed files
with
557 additions
and
238 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'@astrojs/image': minor | ||
--- | ||
|
||
- Fixes two bugs that were blocking SSR support when deployed to a hosting service | ||
- The built-in `sharp` service now automatically rotates images based on EXIF data |
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
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,79 @@ | ||
import fs from 'fs/promises'; | ||
import path from 'path'; | ||
import { fileURLToPath } from 'url'; | ||
import { OUTPUT_DIR } from '../constants.js'; | ||
import { ensureDir } from '../utils/paths.js'; | ||
import { isRemoteImage, loadRemoteImage, loadLocalImage } from '../utils/images.js'; | ||
import type { SSRImageService, TransformOptions } from '../types.js'; | ||
|
||
export interface SSGBuildParams { | ||
loader: SSRImageService; | ||
staticImages: Map<string, Map<string, TransformOptions>>; | ||
srcDir: URL; | ||
outDir: URL; | ||
} | ||
|
||
export async function ssgBuild({ | ||
loader, | ||
staticImages, | ||
srcDir, | ||
outDir, | ||
}: SSGBuildParams) { | ||
const inputFiles = new Set<string>(); | ||
|
||
// process transforms one original image file at a time | ||
for await (const [src, transformsMap] of staticImages) { | ||
let inputFile: string | undefined = undefined; | ||
let inputBuffer: Buffer | undefined = undefined; | ||
|
||
if (isRemoteImage(src)) { | ||
// try to load the remote image | ||
inputBuffer = await loadRemoteImage(src); | ||
} else { | ||
const inputFileURL = new URL(`.${src}`, srcDir); | ||
inputFile = fileURLToPath(inputFileURL); | ||
inputBuffer = await loadLocalImage(inputFile); | ||
|
||
// track the local file used so the original can be copied over | ||
inputFiles.add(inputFile); | ||
} | ||
|
||
if (!inputBuffer) { | ||
// eslint-disable-next-line no-console | ||
console.warn(`"${src}" image could not be fetched`); | ||
continue; | ||
} | ||
|
||
const transforms = Array.from(transformsMap.entries()); | ||
|
||
// process each transformed versiono of the | ||
for await (const [filename, transform] of transforms) { | ||
let outputFile: string; | ||
|
||
if (isRemoteImage(src)) { | ||
const outputFileURL = new URL( | ||
path.join('./', OUTPUT_DIR, path.basename(filename)), | ||
outDir | ||
); | ||
outputFile = fileURLToPath(outputFileURL); | ||
} else { | ||
const outputFileURL = new URL(path.join('./', OUTPUT_DIR, filename), outDir); | ||
outputFile = fileURLToPath(outputFileURL); | ||
} | ||
|
||
const { data } = await loader.transform(inputBuffer, transform); | ||
|
||
ensureDir(path.dirname(outputFile)); | ||
|
||
await fs.writeFile(outputFile, data); | ||
} | ||
} | ||
|
||
// copy all original local images to dist | ||
for await (const original of inputFiles) { | ||
const to = original.replace(fileURLToPath(srcDir), fileURLToPath(outDir)); | ||
|
||
await ensureDir(path.dirname(to)); | ||
await fs.copyFile(original, to); | ||
} | ||
} |
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,29 @@ | ||
import fs from 'fs/promises'; | ||
import path from 'path'; | ||
import glob from 'tiny-glob'; | ||
import { fileURLToPath } from 'url'; | ||
import { ensureDir } from '../utils/paths.js'; | ||
|
||
async function globImages(dir: URL) { | ||
const srcPath = fileURLToPath(dir); | ||
return await glob( | ||
`${srcPath}/**/*.{heic,heif,avif,jpeg,jpg,png,tiff,webp,gif}`, | ||
{ absolute: true } | ||
); | ||
} | ||
|
||
export interface SSRBuildParams { | ||
srcDir: URL; | ||
outDir: URL; | ||
} | ||
|
||
export async function ssrBuild({ srcDir, outDir }: SSRBuildParams) { | ||
const images = await globImages(srcDir); | ||
|
||
for await (const image of images) { | ||
const to = image.replace(fileURLToPath(srcDir), fileURLToPath(outDir)); | ||
|
||
await ensureDir(path.dirname(to)); | ||
await fs.copyFile(image, to); | ||
} | ||
} |
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 |
---|---|---|
@@ -1,137 +1,5 @@ | ||
import type { AstroConfig, AstroIntegration } from 'astro'; | ||
import fs from 'fs/promises'; | ||
import path from 'path'; | ||
import { fileURLToPath } from 'url'; | ||
import { OUTPUT_DIR, PKG_NAME, ROUTE_PATTERN } from './constants.js'; | ||
import sharp from './loaders/sharp.js'; | ||
import { IntegrationOptions, TransformOptions } from './types.js'; | ||
import { | ||
ensureDir, | ||
isRemoteImage, | ||
loadLocalImage, | ||
loadRemoteImage, | ||
propsToFilename, | ||
} from './utils.js'; | ||
import { createPlugin } from './vite-plugin-astro-image.js'; | ||
export * from './get-image.js'; | ||
export * from './get-picture.js'; | ||
import integration from './integration.js'; | ||
export * from './lib/get-image.js'; | ||
export * from './lib/get-picture.js'; | ||
|
||
const createIntegration = (options: IntegrationOptions = {}): AstroIntegration => { | ||
const resolvedOptions = { | ||
serviceEntryPoint: '@astrojs/image/sharp', | ||
...options, | ||
}; | ||
|
||
// During SSG builds, this is used to track all transformed images required. | ||
const staticImages = new Map<string, TransformOptions>(); | ||
|
||
let _config: AstroConfig; | ||
|
||
function getViteConfiguration() { | ||
return { | ||
plugins: [createPlugin(_config, resolvedOptions)], | ||
optimizeDeps: { | ||
include: ['image-size', 'sharp'], | ||
}, | ||
ssr: { | ||
noExternal: ['@astrojs/image', resolvedOptions.serviceEntryPoint], | ||
}, | ||
}; | ||
} | ||
|
||
return { | ||
name: PKG_NAME, | ||
hooks: { | ||
'astro:config:setup': ({ command, config, injectRoute, updateConfig }) => { | ||
_config = config; | ||
|
||
// Always treat `astro dev` as SSR mode, even without an adapter | ||
const mode = command === 'dev' || config.adapter ? 'ssr' : 'ssg'; | ||
|
||
updateConfig({ vite: getViteConfiguration() }); | ||
|
||
// Used to cache all images rendered to HTML | ||
// Added to globalThis to share the same map in Node and Vite | ||
function addStaticImage(transform: TransformOptions) { | ||
staticImages.set(propsToFilename(transform), transform); | ||
} | ||
|
||
// TODO: Add support for custom, user-provided filename format functions | ||
function filenameFormat(transform: TransformOptions, searchParams: URLSearchParams) { | ||
if (mode === 'ssg') { | ||
return isRemoteImage(transform.src) | ||
? path.join(OUTPUT_DIR, path.basename(propsToFilename(transform))) | ||
: path.join( | ||
OUTPUT_DIR, | ||
path.dirname(transform.src), | ||
path.basename(propsToFilename(transform)) | ||
); | ||
} else { | ||
return `${ROUTE_PATTERN}?${searchParams.toString()}`; | ||
} | ||
} | ||
|
||
// Initialize the integration's globalThis namespace | ||
// This is needed to share scope between Node and Vite | ||
globalThis.astroImage = { | ||
loader: undefined, // initialized in first getImage() call | ||
ssrLoader: sharp, | ||
command, | ||
addStaticImage, | ||
filenameFormat, | ||
}; | ||
|
||
if (mode === 'ssr') { | ||
injectRoute({ | ||
pattern: ROUTE_PATTERN, | ||
entryPoint: | ||
command === 'dev' ? '@astrojs/image/endpoints/dev' : '@astrojs/image/endpoints/prod', | ||
}); | ||
} | ||
}, | ||
'astro:build:done': async ({ dir }) => { | ||
for await (const [filename, transform] of staticImages) { | ||
const loader = globalThis.astroImage.loader; | ||
|
||
if (!loader || !('transform' in loader)) { | ||
// this should never be hit, how was a staticImage added without an SSR service? | ||
return; | ||
} | ||
|
||
let inputBuffer: Buffer | undefined = undefined; | ||
let outputFile: string; | ||
|
||
if (isRemoteImage(transform.src)) { | ||
// try to load the remote image | ||
inputBuffer = await loadRemoteImage(transform.src); | ||
|
||
const outputFileURL = new URL( | ||
path.join('./', OUTPUT_DIR, path.basename(filename)), | ||
dir | ||
); | ||
outputFile = fileURLToPath(outputFileURL); | ||
} else { | ||
const inputFileURL = new URL(`.${transform.src}`, _config.srcDir); | ||
const inputFile = fileURLToPath(inputFileURL); | ||
inputBuffer = await loadLocalImage(inputFile); | ||
|
||
const outputFileURL = new URL(path.join('./', OUTPUT_DIR, filename), dir); | ||
outputFile = fileURLToPath(outputFileURL); | ||
} | ||
|
||
if (!inputBuffer) { | ||
// eslint-disable-next-line no-console | ||
console.warn(`"${transform.src}" image could not be fetched`); | ||
continue; | ||
} | ||
|
||
const { data } = await loader.transform(inputBuffer, transform); | ||
ensureDir(path.dirname(outputFile)); | ||
await fs.writeFile(outputFile, data); | ||
} | ||
}, | ||
}, | ||
}; | ||
}; | ||
|
||
export default createIntegration; | ||
export default integration; |
Oops, something went wrong.