Fix experimental.assets's Squoosh service not working on Netlify and Vercel SSR (#6765)

This commit is contained in:
Erika 2023-04-12 10:45:35 +02:00 committed by GitHub
parent 88d465a8d4
commit 6c09ac03bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 52 additions and 113 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Properly include the needed WASM files for the Squoosh service for Netlify and Vercel in SSR

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,4 @@
import { promises as fsp } from 'node:fs'
import { getModuleURL, instantiateEmscriptenWasm, pathify } from './emscripten-utils.js'
import { instantiateEmscriptenWasm } from './emscripten-utils.js'
interface DecodeModule extends EmscriptenWasm.Module {
decode: (data: Uint8Array) => ImageData
@ -35,52 +34,47 @@ export interface RotateOptions {
// MozJPEG
import type { MozJPEGModule as MozJPEGEncodeModule } from './mozjpeg/mozjpeg_enc'
// @ts-ignore
import mozEnc from './mozjpeg/mozjpeg_node_enc.js'
const mozEncWasm = new URL('./mozjpeg/mozjpeg_node_enc.wasm', getModuleURL(import.meta.url))
// @ts-ignore
import mozDec from './mozjpeg/mozjpeg_node_dec.js'
const mozDecWasm = new URL('./mozjpeg/mozjpeg_node_dec.wasm', getModuleURL(import.meta.url))
import mozDecWasm from './mozjpeg/mozjpeg_node_dec.wasm.js'
import mozEnc from './mozjpeg/mozjpeg_node_enc.js'
import mozEncWasm from './mozjpeg/mozjpeg_node_enc.wasm.js'
// WebP
import type { WebPModule as WebPEncodeModule } from './webp/webp_enc'
// @ts-ignore
import webpEnc from './webp/webp_node_enc.js'
const webpEncWasm = new URL('./webp/webp_node_enc.wasm', getModuleURL(import.meta.url))
// @ts-ignore
import webpDec from './webp/webp_node_dec.js'
const webpDecWasm = new URL('./webp/webp_node_dec.wasm', getModuleURL(import.meta.url))
import webpDecWasm from './webp/webp_node_dec.wasm.js'
import webpEnc from './webp/webp_node_enc.js'
import webpEncWasm from './webp/webp_node_enc.wasm.js'
// AVIF
import type { AVIFModule as AVIFEncodeModule } from './avif/avif_enc'
// @ts-ignore
import avifEnc from './avif/avif_node_enc.js'
const avifEncWasm = new URL('./avif/avif_node_enc.wasm', getModuleURL(import.meta.url))
// @ts-ignore
import avifDec from './avif/avif_node_dec.js'
const avifDecWasm = new URL('./avif/avif_node_dec.wasm', getModuleURL(import.meta.url))
import avifDecWasm from './avif/avif_node_dec.wasm.js'
import avifEnc from './avif/avif_node_enc.js'
import avifEncWasm from './avif/avif_node_enc.wasm.js'
// PNG
// @ts-ignore
import * as pngEncDec from './png/squoosh_png.js'
const pngEncDecWasm = new URL('./png/squoosh_png_bg.wasm', getModuleURL(import.meta.url))
import pngEncDecWasm from './png/squoosh_png_bg.wasm.js'
const pngEncDecInit = () =>
pngEncDec.default(fsp.readFile(pathify(pngEncDecWasm.toString())))
pngEncDec.default(pngEncDecWasm)
// OxiPNG
// @ts-ignore
import * as oxipng from './png/squoosh_oxipng.js'
const oxipngWasm = new URL('./png/squoosh_oxipng_bg.wasm', getModuleURL(import.meta.url))
const oxipngInit = () => oxipng.default(fsp.readFile(pathify(oxipngWasm.toString())))
import oxipngWasm from './png/squoosh_oxipng_bg.wasm.js'
const oxipngInit = () => oxipng.default(oxipngWasm)
// Resize
// @ts-ignore
import * as resize from './resize/squoosh_resize.js'
const resizeWasm = new URL('./resize/squoosh_resize_bg.wasm', getModuleURL(import.meta.url))
const resizeInit = () => resize.default(fsp.readFile(pathify(resizeWasm.toString())))
import resizeWasm from './resize/squoosh_resize_bg.wasm.js'
const resizeInit = () => resize.default(resizeWasm)
// rotate
const rotateWasm = new URL('./rotate/rotate.wasm', getModuleURL(import.meta.url))
import rotateWasm from './rotate/rotate.wasm.js'
// Our decoders currently rely on a `ImageData` global.
import ImageData from './image_data.js'
@ -187,7 +181,7 @@ export const preprocessors = {
const sameDimensions = degrees === 0 || degrees === 180
const size = width * height * 4
const instance = (
await WebAssembly.instantiate(await fsp.readFile(pathify(rotateWasm.toString())))
await WebAssembly.instantiate(rotateWasm)
).instance as RotateModuleInstance
const { memory } = instance.exports
const additionalPagesNeeded = Math.ceil(
@ -218,11 +212,11 @@ export const codecs = {
extension: 'jpg',
detectors: [/^\xFF\xD8\xFF/],
dec: () =>
instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm.toString()),
instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm),
enc: () =>
instantiateEmscriptenWasm(
mozEnc as EmscriptenWasm.ModuleFactory<MozJPEGEncodeModule>,
mozEncWasm.toString()
mozEncWasm
),
defaultEncoderOptions: {
quality: 75,
@ -253,11 +247,11 @@ export const codecs = {
extension: 'webp',
detectors: [/^RIFF....WEBPVP8[LX ]/s],
dec: () =>
instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm.toString()),
instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm),
enc: () =>
instantiateEmscriptenWasm(
webpEnc as EmscriptenWasm.ModuleFactory<WebPEncodeModule>,
webpEncWasm.toString()
webpEncWasm
),
defaultEncoderOptions: {
quality: 75,
@ -300,11 +294,11 @@ export const codecs = {
// eslint-disable-next-line no-control-regex
detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/],
dec: () =>
instantiateEmscriptenWasm(avifDec as DecodeModuleFactory, avifDecWasm.toString()),
instantiateEmscriptenWasm(avifDec as DecodeModuleFactory, avifDecWasm),
enc: async () => {
return instantiateEmscriptenWasm(
avifEnc as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
avifEncWasm.toString()
avifEncWasm
)
},
defaultEncoderOptions: {

View file

@ -1,40 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
export async function copyWasmFiles(dir: URL) {
const src = new URL('./', import.meta.url);
const fileList = await listFiles(fileURLToPath(src), fileURLToPath(dir));
for (let file of fileList) {
await fs.mkdir(path.dirname(file.dest), { recursive: true });
await fs.copyFile(file.src, file.dest);
}
}
export async function deleteWasmFiles(dir: URL) {
const src = new URL('./', import.meta.url);
const fileList = await listFiles(fileURLToPath(src), fileURLToPath(dir));
for (let file of fileList) {
await fs.rm(file.dest);
}
}
async function listFiles(src: string, dest: string) {
const itemNames = await fs.readdir(src);
const copiedFiles: {src: string, dest: string}[] = []
await Promise.all(itemNames.map(async (srcName) => {
const srcPath = path.join(src, srcName);
const destPath = path.join(dest, srcName);
const s = await fs.stat(srcPath);
if (s.isFile() && /.wasm$/.test(srcPath)) {
copiedFiles.push({src: srcPath, dest: destPath});
}
else if (s.isDirectory()) {
copiedFiles.push(...await listFiles(srcPath, destPath));
}
}));
return copiedFiles;
}

View file

@ -10,19 +10,14 @@ export function pathify(path: string): string {
export function instantiateEmscriptenWasm<T extends EmscriptenWasm.Module>(
factory: EmscriptenWasm.ModuleFactory<T>,
path: string,
workerJS = ''
bytes: Uint8Array,
): Promise<T> {
return factory({
locateFile(requestPath) {
// The glue code generated by emscripten uses the original
// file names of the worker file and the wasm binary.
// These will have changed in the bundling process and
// we need to inject them here.
if (requestPath.endsWith('.wasm')) return pathify(path)
if (requestPath.endsWith('.worker.js')) return pathify(workerJS)
return requestPath
},
// @ts-expect-error This is a valid Emscripten option, but the type definitions don't know about it
wasmBinary: bytes,
locateFile(file: string) {
return file
}
})
}
@ -32,12 +27,12 @@ export function dirname(url: string) {
/**
* On certain serverless hosts, our ESM bundle is transpiled to CJS before being run, which means
* import.meta.url is undefined, so we'll fall back to __dirname in those cases
* import.meta.url is undefined, so we'll fall back to __filename in those cases
* We should be able to remove this once https://github.com/netlify/zip-it-and-ship-it/issues/750 is fixed
*/
export function getModuleURL(url: string | undefined): string {
if (!url) {
return pathToFileURL(__dirname).toString();
return pathToFileURL(__filename).toString();
}
return url

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
export default Buffer.from("AGFzbQEAAAABDAJgAn9/AGADf39/AAMGBQAAAAABBQMBABAGEQJ/AEGAgMAAC38AQYCAwAALBy4EBm1lbW9yeQIABnJvdGF0ZQAECl9fZGF0YV9lbmQDAAtfX2hlYXBfYmFzZQMBCpsJBUkBAX8gACABbCIAQf////8DcSICBEBBCCEBIABBAnRBCGohAANAIAAgASgCADYCACABQQRqIQEgAEEEaiEAIAJBf2oiAg0ACwsLzQMBFH8gAUECdCERIAAgAWwiDEECdEEEaiESA0ACQAJAAkACQCAEQQFxRQRAIAMgAU8NAiADQQFqIQgMAQsgA0EPaiICIANJIggNASACIAFJIgVFDQEgASADQRBqIAgbIAEgBRshCCACIQMLIAEgA0EQaiICIAIgAUsbIQ0gA0F/cyETIBIgA0ECdGshFEEAIQVBACEOA0ACQAJAIA5FBEAgBSAASQ0BQQEhBAwGCyAAIAVBEGogBUEPaiICIAVJIgcbIAAgAiAASRshBUEBIQQgByACIABPcg0FDAELIAUiAkEBaiEFC0EBIQ4gAyANTw0AIAAgAkEQaiIPIAAgD0kbQQJ0IAJBAnRrIRUgEyABIAJsaiEHIBQgASACQQFqbEECdGohCSADIQoDQCAAIApsIgYgAmoiBEEQaiAAIAZqIA8gAEkbIgYgBEkgDCAGSXINAyAEIAZHBEAgBEECdEEIaiELIBUhBiAHIRAgCSEEA0AgDCABIBBqIhBNDQUgBCALKAIANgIAIAQgEWohBCALQQRqIQsgBkF8aiIGDQALCyAHQX9qIQcgCUF8aiEJIA0gCkEBaiIKRw0ACwwACwALDwsACyAIIQMMAAsAC1MBAX8CQCAAIAFsQQJ0IgJBCGoiAEEIRg0AIAAgAmpBfGohAEEAIQEDQCABIAJGDQEgACABQQhqKAIANgIAIABBfGohACACIAFBBGoiAUcNAAsLC9oDARN/IABBf2ohEEEAIAFBAnRrIREgACABbCIMQQJ0QQhqIRIDQAJAAkACQAJAIARBAXFFBEAgAyABTw0CIANBAWohCQwBCyADQQ9qIgIgA0kiCQ0BIAIgAUkiBUUNASABIANBEGogCRsgASAFGyEJIAIhAwsgASADQRBqIgIgAiABSxshDSASIANBAnRqIRNBACEFQQAhBgNAAkACQCAGQQFxRQRAIAUgAEkNAUEBIQQMBgsgACAFQRBqIAVBD2oiAiAFSSIIGyAAIAIgAEkbIQVBASEEIAggAiAAT3INBQwBCyAFIgJBAWohBQtBASEGIAMgDU8NACAAIAJBEGoiDiAAIA5JG0ECdCACQQJ0ayEUIAMgASAAIAJrbGohCCATIAEgECACa2xBAnRqIQogAyELA0AgACALbCIHIAJqIgRBEGogACAHaiAOIABJGyIHIARJIAwgB0lyDQMgBCAHRwRAIARBAnRBCGohBiAUIQcgCCEPIAohBANAIAwgDyABayIPTQ0FIAQgBigCADYCACAEIBFqIQQgBkEEaiEGIAdBfGoiBw0ACwtBASEGIAhBAWohCCAKQQRqIQogDSALQQFqIgtHDQALDAALAAsPCwALIAkhAwwACwALUAACQAJAAkACQCACQbMBTARAIAJFDQIgAkHaAEcNASAAIAEQAQ8LIAJBtAFGDQIgAkGOAkYNAwsACyAAIAEQAA8LIAAgARACDwsgACABEAMLAE0JcHJvZHVjZXJzAghsYW5ndWFnZQEEUnVzdAAMcHJvY2Vzc2VkLWJ5AQVydXN0Yx0xLjQ3LjAgKDE4YmY2YjRmMCAyMDIwLTEwLTA3KQ==", 'base64');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -12,7 +12,6 @@ import { appendForwardSlash, joinPaths, prependForwardSlash } from '../core/path
import { VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js';
import { isESMImportedImage } from './internal.js';
import { isLocalService } from './services/service.js';
import { copyWasmFiles } from './services/vendor/squoosh/copy-wasm.js';
import { emitESMImage } from './utils/emitAsset.js';
import { imageMetadata } from './utils/metadata.js';
import { getOrigQueryParams } from './utils/queryParams.js';
@ -181,20 +180,6 @@ export default function assets({
}
};
},
async buildEnd() {
if (mode != 'build') {
return;
}
if (settings.config.image.service === 'astro/assets/services/squoosh') {
const dir =
settings.config.output === 'server'
? settings.config.build.server
: settings.config.outDir;
await copyWasmFiles(new URL('./chunks', dir));
}
},
// In build, rewrite paths to ESM imported images in code to their final location
async renderChunk(code) {
const assetUrlRE = /__ASTRO_ASSET_IMAGE__([a-z\d]{8})__(?:_(.*?)__)?/g;
@ -237,6 +222,6 @@ export default function assets({
return `export default ${JSON.stringify(meta)}`;
}
},
},
}
];
}

View file

@ -17,7 +17,6 @@ import {
generateImage as generateImageInternal,
getStaticImageList,
} from '../../assets/internal.js';
import { deleteWasmFiles } from '../../assets/services/vendor/squoosh/copy-wasm.js';
import { hasPrerenderedPages, type BuildInternals } from '../../core/build/internal.js';
import {
prependForwardSlash,
@ -115,15 +114,6 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
await generateImage(opts, imageData[1].options, imageData[1].path);
}
// Our Squoosh image service loads `.wasm` files relatively, so we need to copy the WASM files to the dist
// for the image generation to work. In static output, we can remove those after the build is done.
if (
opts.settings.config.image.service === 'astro/assets/services/squoosh' &&
opts.settings.config.output === 'static'
) {
await deleteWasmFiles(new URL('./chunks', opts.settings.config.outDir));
}
delete globalThis.astroAsset.addStaticImage;
}