diff --git a/.changeset/shy-walls-divide.md b/.changeset/shy-walls-divide.md new file mode 100644 index 000000000000..e2b139068f21 --- /dev/null +++ b/.changeset/shy-walls-divide.md @@ -0,0 +1,6 @@ +--- +'astro': patch +'@astrojs/markdown-remark': patch +--- + +Vendor `image-size` to fix CJS-related issues diff --git a/packages/astro/package.json b/packages/astro/package.json index 9bcd9cd1e65d..f4a841868450 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -94,9 +94,9 @@ ], "scripts": { "prebuild": "astro-scripts prebuild --to-string \"src/runtime/server/astro-island.ts\" \"src/runtime/client/{idle,load,media,only,visible}.ts\"", - "build": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\" && tsc && pnpm run postbuild", - "build:ci": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\" && pnpm run postbuild", - "dev": "astro-scripts dev --copy-wasm --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.ts\"", + "build": "pnpm run prebuild && astro-scripts build \"src/**/*.{ts,js}\" && tsc && pnpm run postbuild", + "build:ci": "pnpm run prebuild && astro-scripts build \"src/**/*.{ts,js}\" && pnpm run postbuild", + "dev": "astro-scripts dev --copy-wasm --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.{ts,js}\"", "postbuild": "astro-scripts copy \"src/**/*.astro\" && astro-scripts copy \"src/**/*.wasm\"", "test:unit": "mocha --exit --timeout 30000 ./test/units/**/*.test.js", "test:unit:match": "mocha --exit --timeout 30000 ./test/units/**/*.test.js -g", @@ -136,7 +136,6 @@ "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "html-escaper": "^3.0.3", - "image-size": "^1.0.2", "kleur": "^4.1.4", "magic-string": "^0.27.0", "mime": "^3.0.0", diff --git a/packages/astro/src/assets/utils/metadata.ts b/packages/astro/src/assets/utils/metadata.ts index e276b0899911..b751ac970cf0 100644 --- a/packages/astro/src/assets/utils/metadata.ts +++ b/packages/astro/src/assets/utils/metadata.ts @@ -1,19 +1,16 @@ import fs from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; +import imageSize from '../vendor/image-size/index.js'; import type { ImageMetadata, InputFormat } from '../types.js'; export interface Metadata extends ImageMetadata { orientation?: number; } -let sizeOf: typeof import('image-size').default | undefined; export async function imageMetadata( src: URL | string, data?: Buffer ): Promise { - if (!sizeOf) { - sizeOf = await import('image-size').then((mod) => mod.default); - } let file = data; if (!file) { try { @@ -23,7 +20,7 @@ export async function imageMetadata( } } - const { width, height, type, orientation } = await sizeOf!(file); + const { width, height, type, orientation } = imageSize(file); const isPortrait = (orientation || 0) >= 5; if (!width || !height || !type) { diff --git a/packages/astro/src/assets/vendor/README.md b/packages/astro/src/assets/vendor/README.md new file mode 100644 index 000000000000..7b6927b674b3 --- /dev/null +++ b/packages/astro/src/assets/vendor/README.md @@ -0,0 +1,3 @@ +Vendored version of `image-size` and `queue` because we had issues with the CJS nature of those packages. + +Should hopefully be fixed by https://github.com/image-size/image-size/pull/370 diff --git a/packages/astro/src/assets/vendor/image-size/LICENSE b/packages/astro/src/assets/vendor/image-size/LICENSE new file mode 100644 index 000000000000..1341a90d565f --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright © 2017 Aditya Yadav, http://netroy.in + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/astro/src/assets/vendor/image-size/detector.ts b/packages/astro/src/assets/vendor/image-size/detector.ts new file mode 100644 index 000000000000..a6cacd28f7fa --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/detector.ts @@ -0,0 +1,30 @@ +import { imageType, typeHandlers } from './types.js' + +const keys = Object.keys(typeHandlers) as imageType[] + +// This map helps avoid validating for every single image type +const firstBytes: { [byte: number]: imageType } = { + 0x38: 'psd', + 0x42: 'bmp', + 0x44: 'dds', + 0x47: 'gif', + 0x49: 'tiff', + 0x4d: 'tiff', + 0x52: 'webp', + 0x69: 'icns', + 0x89: 'png', + 0xff: 'jpg' +} + +export function detector(buffer: Buffer): imageType | undefined { + const byte = buffer[0] + if (byte in firstBytes) { + const type = firstBytes[byte] + if (type && typeHandlers[type].validate(buffer)) { + return type + } + } + + const finder = (key: imageType) => typeHandlers[key].validate(buffer) + return keys.find(finder) +} diff --git a/packages/astro/src/assets/vendor/image-size/index.ts b/packages/astro/src/assets/vendor/image-size/index.ts new file mode 100644 index 000000000000..961ebb0b3959 --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/index.ts @@ -0,0 +1,146 @@ +import * as fs from "fs"; +import * as path from "path"; +import Queue from "../queue/queue.js"; +import { detector } from "./detector.js"; +import { imageType, typeHandlers } from "./types.js"; +import type { ISizeCalculationResult } from "./types/interface.js"; + +type CallbackFn = (e: Error | null, r?: ISizeCalculationResult) => void; + +// Maximum buffer size, with a default of 512 kilobytes. +// TO-DO: make this adaptive based on the initial signature of the image +const MaxBufferSize = 512 * 1024; + +// This queue is for async `fs` operations, to avoid reaching file-descriptor limits +const queue = new Queue({ concurrency: 100, autostart: true }); + +interface Options { + disabledFS: boolean; + disabledTypes: imageType[]; +} + +const globalOptions: Options = { + disabledFS: false, + disabledTypes: [], +}; + +/** + * Return size information based on a buffer + * + * @param {Buffer} buffer + * @param {String} filepath + * @returns {Object} + */ +function lookup(buffer: Buffer, filepath?: string): ISizeCalculationResult { + // detect the file type.. don't rely on the extension + const type = detector(buffer); + + if (typeof type !== "undefined") { + if (globalOptions.disabledTypes.indexOf(type) > -1) { + throw new TypeError("disabled file type: " + type); + } + + // find an appropriate handler for this file type + if (type in typeHandlers) { + const size = typeHandlers[type].calculate(buffer, filepath); + if (size !== undefined) { + size.type = type; + return size; + } + } + } + + // throw up, if we don't understand the file + throw new TypeError( + "unsupported file type: " + type + " (file: " + filepath + ")" + ); +} + +/** + * Reads a file into a buffer. + * @param {String} filepath + * @returns {Promise} + */ +async function asyncFileToBuffer(filepath: string): Promise { + const handle = await fs.promises.open(filepath, "r"); + const { size } = await handle.stat(); + if (size <= 0) { + await handle.close(); + throw new Error("Empty file"); + } + const bufferSize = Math.min(size, MaxBufferSize); + const buffer = Buffer.alloc(bufferSize); + await handle.read(buffer, 0, bufferSize, 0); + await handle.close(); + return buffer; +} + +/** + * Synchronously reads a file into a buffer, blocking the nodejs process. + * + * @param {String} filepath + * @returns {Buffer} + */ +function syncFileToBuffer(filepath: string): Buffer { + // read from the file, synchronously + const descriptor = fs.openSync(filepath, "r"); + const { size } = fs.fstatSync(descriptor); + if (size <= 0) { + fs.closeSync(descriptor); + throw new Error("Empty file"); + } + const bufferSize = Math.min(size, MaxBufferSize); + const buffer = Buffer.alloc(bufferSize); + fs.readSync(descriptor, buffer, 0, bufferSize, 0); + fs.closeSync(descriptor); + return buffer; +} + +export default imageSize; +export function imageSize(input: Buffer | string): ISizeCalculationResult; +export function imageSize(input: string, callback: CallbackFn): void; + +/** + * @param {Buffer|string} input - buffer or relative/absolute path of the image file + * @param {Function=} [callback] - optional function for async detection + */ +export function imageSize( + input: Buffer | string, + callback?: CallbackFn +): ISizeCalculationResult | void { + // Handle buffer input + if (Buffer.isBuffer(input)) { + return lookup(input); + } + + // input should be a string at this point + if (typeof input !== "string" || globalOptions.disabledFS) { + throw new TypeError("invalid invocation. input should be a Buffer"); + } + + // resolve the file path + const filepath = path.resolve(input); + if (typeof callback === "function") { + queue.push(() => + asyncFileToBuffer(filepath) + .then((buffer) => + process.nextTick(callback, null, lookup(buffer, filepath)) + ) + .catch(callback) + ); + } else { + const buffer = syncFileToBuffer(filepath); + return lookup(buffer, filepath); + } +} + +export const disableFS = (v: boolean): void => { + globalOptions.disabledFS = v; +}; +export const disableTypes = (types: imageType[]): void => { + globalOptions.disabledTypes = types; +}; +export const setConcurrency = (c: number): void => { + queue.concurrency = c; +}; +export const types = Object.keys(typeHandlers); diff --git a/packages/astro/src/assets/vendor/image-size/readUInt.ts b/packages/astro/src/assets/vendor/image-size/readUInt.ts new file mode 100644 index 000000000000..ede811408a75 --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/readUInt.ts @@ -0,0 +1,10 @@ +type Bits = 16 | 32 +type MethodName = 'readUInt16BE' | 'readUInt16LE' | 'readUInt32BE' | 'readUInt32LE' + +// Abstract reading multi-byte unsigned integers +export function readUInt(buffer: Buffer, bits: Bits, offset: number, isBigEndian: boolean): number { + offset = offset || 0 + const endian = isBigEndian ? 'BE' : 'LE' + const methodName: MethodName = ('readUInt' + bits + endian) as MethodName + return buffer[methodName].call(buffer, offset) +} diff --git a/packages/astro/src/assets/vendor/image-size/types.ts b/packages/astro/src/assets/vendor/image-size/types.ts new file mode 100644 index 000000000000..05f4f82cc4e0 --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types.ts @@ -0,0 +1,38 @@ +// load all available handlers explicitely for browserify support +import { BMP } from './types/bmp.js' +import { CUR } from './types/cur.js' +import { DDS } from './types/dds.js' +import { GIF } from './types/gif.js' +import { ICNS } from './types/icns.js' +import { ICO } from './types/ico.js' +import { J2C } from './types/j2c.js' +import { JP2 } from './types/jp2.js' +import { JPG } from './types/jpg.js' +import { KTX } from './types/ktx.js' +import { PNG } from './types/png.js' +import { PNM } from './types/pnm.js' +import { PSD } from './types/psd.js' +import { SVG } from './types/svg.js' +import { TIFF } from './types/tiff.js' +import { WEBP } from './types/webp.js' + +export const typeHandlers = { + bmp: BMP, + cur: CUR, + dds: DDS, + gif: GIF, + icns: ICNS, + ico: ICO, + j2c: J2C, + jp2: JP2, + jpg: JPG, + ktx: KTX, + png: PNG, + pnm: PNM, + psd: PSD, + svg: SVG, + tiff: TIFF, + webp: WEBP, +} + +export type imageType = keyof typeof typeHandlers diff --git a/packages/astro/src/assets/vendor/image-size/types/bmp.ts b/packages/astro/src/assets/vendor/image-size/types/bmp.ts new file mode 100644 index 000000000000..2f55ccdd1057 --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/bmp.ts @@ -0,0 +1,14 @@ +import type { IImage } from './interface' + +export const BMP: IImage = { + validate(buffer) { + return ('BM' === buffer.toString('ascii', 0, 2)) + }, + + calculate(buffer) { + return { + height: Math.abs(buffer.readInt32LE(22)), + width: buffer.readUInt32LE(18) + } + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/cur.ts b/packages/astro/src/assets/vendor/image-size/types/cur.ts new file mode 100644 index 000000000000..42766baecd1f --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/cur.ts @@ -0,0 +1,16 @@ +import { ICO } from './ico.js' +import type { IImage } from './interface' + +const TYPE_CURSOR = 2 +export const CUR: IImage = { + validate(buffer) { + if (buffer.readUInt16LE(0) !== 0) { + return false + } + return buffer.readUInt16LE(2) === TYPE_CURSOR + }, + + calculate(buffer) { + return ICO.calculate(buffer) + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/dds.ts b/packages/astro/src/assets/vendor/image-size/types/dds.ts new file mode 100644 index 000000000000..e9ceb63ba6e3 --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/dds.ts @@ -0,0 +1,14 @@ +import type { IImage } from './interface' + +export const DDS: IImage = { + validate(buffer) { + return buffer.readUInt32LE(0) === 0x20534444 + }, + + calculate(buffer) { + return { + height: buffer.readUInt32LE(12), + width: buffer.readUInt32LE(16) + } + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/gif.ts b/packages/astro/src/assets/vendor/image-size/types/gif.ts new file mode 100644 index 000000000000..b18b305f199e --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/gif.ts @@ -0,0 +1,16 @@ +import type { IImage } from './interface' + +const gifRegexp = /^GIF8[79]a/ +export const GIF: IImage = { + validate(buffer) { + const signature = buffer.toString('ascii', 0, 6) + return (gifRegexp.test(signature)) + }, + + calculate(buffer) { + return { + height: buffer.readUInt16LE(8), + width: buffer.readUInt16LE(6) + } + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/icns.ts b/packages/astro/src/assets/vendor/image-size/types/icns.ts new file mode 100644 index 000000000000..5beccb02c47d --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/icns.ts @@ -0,0 +1,113 @@ +import type { IImage, ISize } from './interface' + +/** + * ICNS Header + * + * | Offset | Size | Purpose | + * | 0 | 4 | Magic literal, must be "icns" (0x69, 0x63, 0x6e, 0x73) | + * | 4 | 4 | Length of file, in bytes, msb first. | + * + */ +const SIZE_HEADER = 4 + 4 // 8 +const FILE_LENGTH_OFFSET = 4 // MSB => BIG ENDIAN + +/** + * Image Entry + * + * | Offset | Size | Purpose | + * | 0 | 4 | Icon type, see OSType below. | + * | 4 | 4 | Length of data, in bytes (including type and length), msb first. | + * | 8 | n | Icon data | + */ +const ENTRY_LENGTH_OFFSET = 4 // MSB => BIG ENDIAN + +const ICON_TYPE_SIZE: {[key: string]: number} = { + ICON: 32, + 'ICN#': 32, + // m => 16 x 16 + 'icm#': 16, + icm4: 16, + icm8: 16, + // s => 16 x 16 + 'ics#': 16, + ics4: 16, + ics8: 16, + is32: 16, + s8mk: 16, + icp4: 16, + // l => 32 x 32 + icl4: 32, + icl8: 32, + il32: 32, + l8mk: 32, + icp5: 32, + ic11: 32, + // h => 48 x 48 + ich4: 48, + ich8: 48, + ih32: 48, + h8mk: 48, + // . => 64 x 64 + icp6: 64, + ic12: 32, + // t => 128 x 128 + it32: 128, + t8mk: 128, + ic07: 128, + // . => 256 x 256 + ic08: 256, + ic13: 256, + // . => 512 x 512 + ic09: 512, + ic14: 512, + // . => 1024 x 1024 + ic10: 1024, +} + +function readImageHeader(buffer: Buffer, imageOffset: number): [string, number] { + const imageLengthOffset = imageOffset + ENTRY_LENGTH_OFFSET + return [ + buffer.toString('ascii', imageOffset, imageLengthOffset), + buffer.readUInt32BE(imageLengthOffset) + ] +} + +function getImageSize(type: string): ISize { + const size = ICON_TYPE_SIZE[type] + return { width: size, height: size, type } +} + +export const ICNS: IImage = { + validate(buffer) { + return ('icns' === buffer.toString('ascii', 0, 4)) + }, + + calculate(buffer) { + const bufferLength = buffer.length + const fileLength = buffer.readUInt32BE(FILE_LENGTH_OFFSET) + let imageOffset = SIZE_HEADER + + let imageHeader = readImageHeader(buffer, imageOffset) + let imageSize = getImageSize(imageHeader[0]) + imageOffset += imageHeader[1] + + if (imageOffset === fileLength) { + return imageSize + } + + const result = { + height: imageSize.height, + images: [imageSize], + width: imageSize.width + } + + while (imageOffset < fileLength && imageOffset < bufferLength) { + imageHeader = readImageHeader(buffer, imageOffset) + imageSize = getImageSize(imageHeader[0]) + imageOffset += imageHeader[1] + result.images.push(imageSize) + } + + return result + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/ico.ts b/packages/astro/src/assets/vendor/image-size/types/ico.ts new file mode 100644 index 000000000000..09310176fcb9 --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/ico.ts @@ -0,0 +1,76 @@ +import type { IImage, ISize, ISizeCalculationResult } from './interface' + +const TYPE_ICON = 1 + +/** + * ICON Header + * + * | Offset | Size | Purpose | + * | 0 | 2 | Reserved. Must always be 0. | + * | 2 | 2 | Image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid. | + * | 4 | 2 | Number of images in the file. | + * + */ +const SIZE_HEADER = 2 + 2 + 2 // 6 + +/** + * Image Entry + * + * | Offset | Size | Purpose | + * | 0 | 1 | Image width in pixels. Can be any number between 0 and 255. Value 0 means width is 256 pixels. | + * | 1 | 1 | Image height in pixels. Can be any number between 0 and 255. Value 0 means height is 256 pixels. | + * | 2 | 1 | Number of colors in the color palette. Should be 0 if the image does not use a color palette. | + * | 3 | 1 | Reserved. Should be 0. | + * | 4 | 2 | ICO format: Color planes. Should be 0 or 1. | + * | | | CUR format: The horizontal coordinates of the hotspot in number of pixels from the left. | + * | 6 | 2 | ICO format: Bits per pixel. | + * | | | CUR format: The vertical coordinates of the hotspot in number of pixels from the top. | + * | 8 | 4 | The size of the image's data in bytes | + * | 12 | 4 | The offset of BMP or PNG data from the beginning of the ICO/CUR file | + * + */ +const SIZE_IMAGE_ENTRY = 1 + 1 + 1 + 1 + 2 + 2 + 4 + 4 // 16 + +function getSizeFromOffset(buffer: Buffer, offset: number): number { + const value = buffer.readUInt8(offset) + return value === 0 ? 256 : value +} + +function getImageSize(buffer: Buffer, imageIndex: number): ISize { + const offset = SIZE_HEADER + (imageIndex * SIZE_IMAGE_ENTRY) + return { + height: getSizeFromOffset(buffer, offset + 1), + width: getSizeFromOffset(buffer, offset) + } +} + +export const ICO: IImage = { + validate(buffer) { + if (buffer.readUInt16LE(0) !== 0) { + return false + } + return buffer.readUInt16LE(2) === TYPE_ICON + }, + + calculate(buffer) { + const nbImages = buffer.readUInt16LE(4) + const imageSize = getImageSize(buffer, 0) + + if (nbImages === 1) { + return imageSize + } + + const imgs: ISize[] = [imageSize] + for (let imageIndex = 1; imageIndex < nbImages; imageIndex += 1) { + imgs.push(getImageSize(buffer, imageIndex)) + } + + const result: ISizeCalculationResult = { + height: imageSize.height, + images: imgs, + width: imageSize.width + } + + return result + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/interface.ts b/packages/astro/src/assets/vendor/image-size/types/interface.ts new file mode 100644 index 000000000000..9886b357361e --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/interface.ts @@ -0,0 +1,15 @@ +export interface ISize { + width: number | undefined + height: number | undefined + orientation?: number + type?: string +} + +export interface ISizeCalculationResult extends ISize { + images?: ISize[] +} + +export interface IImage { + validate: (buffer: Buffer) => boolean + calculate: (buffer: Buffer, filepath?: string) => ISizeCalculationResult +} diff --git a/packages/astro/src/assets/vendor/image-size/types/j2c.ts b/packages/astro/src/assets/vendor/image-size/types/j2c.ts new file mode 100644 index 000000000000..301c38cb6f06 --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/j2c.ts @@ -0,0 +1,15 @@ +import type { IImage } from './interface' + +export const J2C: IImage = { + validate(buffer) { + // TODO: this doesn't seem right. SIZ marker doesn't have to be right after the SOC + return buffer.toString('hex', 0, 4) === 'ff4fff51' + }, + + calculate(buffer) { + return { + height: buffer.readUInt32BE(12), + width: buffer.readUInt32BE(8), + } + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/jp2.ts b/packages/astro/src/assets/vendor/image-size/types/jp2.ts new file mode 100644 index 000000000000..e23e1aced0ba --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/jp2.ts @@ -0,0 +1,62 @@ +import type { IImage, ISize } from './interface' + +const BoxTypes = { + ftyp: '66747970', + ihdr: '69686472', + jp2h: '6a703268', + jp__: '6a502020', + rreq: '72726571', + xml_: '786d6c20' +} + +const calculateRREQLength = (box: Buffer): number => { + const unit = box.readUInt8(0) + let offset = 1 + (2 * unit) + const numStdFlags = box.readUInt16BE(offset) + const flagsLength = numStdFlags * (2 + unit) + offset = offset + 2 + flagsLength + const numVendorFeatures = box.readUInt16BE(offset) + const featuresLength = numVendorFeatures * (16 + unit) + return offset + 2 + featuresLength +} + +const parseIHDR = (box: Buffer): ISize => { + return { + height: box.readUInt32BE(4), + width: box.readUInt32BE(8), + } +} + +export const JP2: IImage = { + validate(buffer) { + const signature = buffer.toString('hex', 4, 8) + const signatureLength = buffer.readUInt32BE(0) + if (signature !== BoxTypes.jp__ || signatureLength < 1) { + return false + } + + const ftypeBoxStart = signatureLength + 4 + const ftypBoxLength = buffer.readUInt32BE(signatureLength) + const ftypBox = buffer.slice(ftypeBoxStart, ftypeBoxStart + ftypBoxLength) + return ftypBox.toString('hex', 0, 4) === BoxTypes.ftyp + }, + + calculate(buffer) { + const signatureLength = buffer.readUInt32BE(0) + const ftypBoxLength = buffer.readUInt16BE(signatureLength + 2) + let offset = signatureLength + 4 + ftypBoxLength + const nextBoxType = buffer.toString('hex', offset, offset + 4) + switch (nextBoxType) { + case BoxTypes.rreq: + // WHAT ARE THESE 4 BYTES????? + // eslint-disable-next-line no-case-declarations + const MAGIC = 4 + offset = offset + 4 + MAGIC + calculateRREQLength(buffer.slice(offset + 4)) + return parseIHDR(buffer.slice(offset + 8, offset + 24)) + case BoxTypes.jp2h : + return parseIHDR(buffer.slice(offset + 8, offset + 24)) + default: + throw new TypeError('Unsupported header found: ' + buffer.toString('ascii', offset, offset + 4)) + } + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/jpg.ts b/packages/astro/src/assets/vendor/image-size/types/jpg.ts new file mode 100644 index 000000000000..68c32b7bec9c --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/jpg.ts @@ -0,0 +1,151 @@ +// NOTE: we only support baseline and progressive JPGs here +// due to the structure of the loader class, we only get a buffer +// with a maximum size of 4096 bytes. so if the SOF marker is outside +// if this range we can't detect the file size correctly. + +import { readUInt } from '../readUInt.js' +import type { IImage, ISize } from './interface' + +const EXIF_MARKER = '45786966' +const APP1_DATA_SIZE_BYTES = 2 +const EXIF_HEADER_BYTES = 6 +const TIFF_BYTE_ALIGN_BYTES = 2 +const BIG_ENDIAN_BYTE_ALIGN = '4d4d' +const LITTLE_ENDIAN_BYTE_ALIGN = '4949' + +// Each entry is exactly 12 bytes +const IDF_ENTRY_BYTES = 12 +const NUM_DIRECTORY_ENTRIES_BYTES = 2 + +function isEXIF(buffer: Buffer): boolean { + return (buffer.toString('hex', 2, 6) === EXIF_MARKER) +} + +function extractSize(buffer: Buffer, index: number): ISize { + return { + height : buffer.readUInt16BE(index), + width : buffer.readUInt16BE(index + 2) + } +} + +function extractOrientation(exifBlock: Buffer, isBigEndian: boolean) { + // TODO: assert that this contains 0x002A + // let STATIC_MOTOROLA_TIFF_HEADER_BYTES = 2 + // let TIFF_IMAGE_FILE_DIRECTORY_BYTES = 4 + + // TODO: derive from TIFF_IMAGE_FILE_DIRECTORY_BYTES + const idfOffset = 8 + + // IDF osset works from right after the header bytes + // (so the offset includes the tiff byte align) + const offset = EXIF_HEADER_BYTES + idfOffset + + const idfDirectoryEntries = readUInt(exifBlock, 16, offset, isBigEndian) + + for (let directoryEntryNumber = 0; directoryEntryNumber < idfDirectoryEntries; directoryEntryNumber++) { + const start = offset + NUM_DIRECTORY_ENTRIES_BYTES + (directoryEntryNumber * IDF_ENTRY_BYTES) + const end = start + IDF_ENTRY_BYTES + + // Skip on corrupt EXIF blocks + if (start > exifBlock.length) { + return + } + + const block = exifBlock.slice(start, end) + const tagNumber = readUInt(block, 16, 0, isBigEndian) + + // 0x0112 (decimal: 274) is the `orientation` tag ID + if (tagNumber === 274) { + const dataFormat = readUInt(block, 16, 2, isBigEndian) + if (dataFormat !== 3) { + return + } + + // unsinged int has 2 bytes per component + // if there would more than 4 bytes in total it's a pointer + const numberOfComponents = readUInt(block, 32, 4, isBigEndian) + if (numberOfComponents !== 1) { + return + } + + return readUInt(block, 16, 8, isBigEndian) + } + } +} + +function validateExifBlock(buffer: Buffer, index: number) { + // Skip APP1 Data Size + const exifBlock = buffer.slice(APP1_DATA_SIZE_BYTES, index) + + // Consider byte alignment + const byteAlign = exifBlock.toString('hex', EXIF_HEADER_BYTES, EXIF_HEADER_BYTES + TIFF_BYTE_ALIGN_BYTES) + + // Ignore Empty EXIF. Validate byte alignment + const isBigEndian = byteAlign === BIG_ENDIAN_BYTE_ALIGN + const isLittleEndian = byteAlign === LITTLE_ENDIAN_BYTE_ALIGN + + if (isBigEndian || isLittleEndian) { + return extractOrientation(exifBlock, isBigEndian) + } +} + +function validateBuffer(buffer: Buffer, index: number): void { + // index should be within buffer limits + if (index > buffer.length) { + throw new TypeError('Corrupt JPG, exceeded buffer limits') + } + // Every JPEG block must begin with a 0xFF + if (buffer[index] !== 0xFF) { + throw new TypeError('Invalid JPG, marker table corrupted') + } +} + +export const JPG: IImage = { + validate(buffer) { + const SOIMarker = buffer.toString('hex', 0, 2) + return ('ffd8' === SOIMarker) + }, + + calculate(buffer) { + // Skip 4 chars, they are for signature + buffer = buffer.slice(4) + + let orientation: number | undefined + let next: number + while (buffer.length) { + // read length of the next block + const i = buffer.readUInt16BE(0) + + if (isEXIF(buffer)) { + orientation = validateExifBlock(buffer, i) + } + + // ensure correct format + validateBuffer(buffer, i) + + // 0xFFC0 is baseline standard(SOF) + // 0xFFC1 is baseline optimized(SOF) + // 0xFFC2 is progressive(SOF2) + next = buffer[i + 1] + if (next === 0xC0 || next === 0xC1 || next === 0xC2) { + const size = extractSize(buffer, i + 5) + + // TODO: is orientation=0 a valid answer here? + if (!orientation) { + return size + } + + return { + height: size.height, + orientation, + width: size.width + } + } + + // move to the next block + buffer = buffer.slice(i + 2) + } + + throw new TypeError('Invalid JPG, no size found') + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/ktx.ts b/packages/astro/src/assets/vendor/image-size/types/ktx.ts new file mode 100644 index 000000000000..9e43fdeaaf37 --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/ktx.ts @@ -0,0 +1,16 @@ +import type { IImage } from './interface' + +const SIGNATURE = 'KTX 11' + +export const KTX: IImage = { + validate(buffer) { + return SIGNATURE === buffer.toString('ascii', 1, 7) + }, + + calculate(buffer) { + return { + height: buffer.readUInt32LE(40), + width: buffer.readUInt32LE(36), + } + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/png.ts b/packages/astro/src/assets/vendor/image-size/types/png.ts new file mode 100644 index 000000000000..a31411380c3e --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/png.ts @@ -0,0 +1,36 @@ +import type { IImage } from './interface' + +const pngSignature = 'PNG\r\n\x1a\n' +const pngImageHeaderChunkName = 'IHDR' + +// Used to detect "fried" png's: http://www.jongware.com/pngdefry.html +const pngFriedChunkName = 'CgBI' + +export const PNG: IImage = { + validate(buffer) { + if (pngSignature === buffer.toString('ascii', 1, 8)) { + let chunkName = buffer.toString('ascii', 12, 16) + if (chunkName === pngFriedChunkName) { + chunkName = buffer.toString('ascii', 28, 32) + } + if (chunkName !== pngImageHeaderChunkName) { + throw new TypeError('Invalid PNG') + } + return true + } + return false + }, + + calculate(buffer) { + if (buffer.toString('ascii', 12, 16) === pngFriedChunkName) { + return { + height: buffer.readUInt32BE(36), + width: buffer.readUInt32BE(32) + } + } + return { + height: buffer.readUInt32BE(20), + width: buffer.readUInt32BE(16) + } + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/pnm.ts b/packages/astro/src/assets/vendor/image-size/types/pnm.ts new file mode 100644 index 000000000000..687c6265e9f7 --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/pnm.ts @@ -0,0 +1,80 @@ +import type { IImage, ISize } from './interface' + +const PNMTypes: { [signature: string]: string } = { + P1: 'pbm/ascii', + P2: 'pgm/ascii', + P3: 'ppm/ascii', + P4: 'pbm', + P5: 'pgm', + P6: 'ppm', + P7: 'pam', + PF: 'pfm' +} + +const Signatures = Object.keys(PNMTypes) + +type Handler = (type: string[]) => ISize +const handlers: { [type: string]: Handler} = { + default: (lines) => { + let dimensions: string[] = [] + + while (lines.length > 0) { + const line = lines.shift() as string + if (line[0] === '#') { + continue + } + dimensions = line.split(' ') + break + } + + if (dimensions.length === 2) { + return { + height: parseInt(dimensions[1], 10), + width: parseInt(dimensions[0], 10), + } + } else { + throw new TypeError('Invalid PNM') + } + }, + pam: (lines) => { + const size: { [key: string]: number } = {} + while (lines.length > 0) { + const line = lines.shift() as string + if (line.length > 16 || line.charCodeAt(0) > 128) { + continue + } + const [key, value] = line.split(' ') + if (key && value) { + size[key.toLowerCase()] = parseInt(value, 10) + } + if (size.height && size.width) { + break + } + } + + if (size.height && size.width) { + return { + height: size.height, + width: size.width + } + } else { + throw new TypeError('Invalid PAM') + } + } +} + +export const PNM: IImage = { + validate(buffer) { + const signature = buffer.toString('ascii', 0, 2) + return Signatures.includes(signature) + }, + + calculate(buffer) { + const signature = buffer.toString('ascii', 0, 2) + const type = PNMTypes[signature] + // TODO: this probably generates garbage. move to a stream based parser + const lines = buffer.toString('ascii', 3).split(/[\r\n]+/) + const handler = handlers[type] || handlers.default + return handler(lines) + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/psd.ts b/packages/astro/src/assets/vendor/image-size/types/psd.ts new file mode 100644 index 000000000000..7521f5e9f826 --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/psd.ts @@ -0,0 +1,14 @@ +import type { IImage } from './interface' + +export const PSD: IImage = { + validate(buffer) { + return ('8BPS' === buffer.toString('ascii', 0, 4)) + }, + + calculate(buffer) { + return { + height: buffer.readUInt32BE(14), + width: buffer.readUInt32BE(18) + } + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/svg.ts b/packages/astro/src/assets/vendor/image-size/types/svg.ts new file mode 100644 index 000000000000..7cb164679d89 --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/svg.ts @@ -0,0 +1,106 @@ +import type { IImage, ISize } from './interface' + +interface IAttributes { + width: number | null + height: number | null + viewbox?: IAttributes | null +} + +const svgReg = /"']|"[^"]*"|'[^']*')*>/ + +const extractorRegExps = { + height: /\sheight=(['"])([^%]+?)\1/, + root: svgReg, + viewbox: /\sviewBox=(['"])(.+?)\1/i, + width: /\swidth=(['"])([^%]+?)\1/, +} + +const INCH_CM = 2.54 +const units: { [unit: string]: number } = { + in: 96, + cm: 96 / INCH_CM, + em: 16, + ex: 8, + m: 96 / INCH_CM * 100, + mm: 96 / INCH_CM / 10, + pc: 96 / 72 / 12, + pt: 96 / 72, + px: 1 +} + +const unitsReg = new RegExp(`^([0-9.]+(?:e\\d+)?)(${Object.keys(units).join('|')})?$`) + +function parseLength(len: string) { + const m = unitsReg.exec(len) + if (!m) { + return undefined + } + return Math.round(Number(m[1]) * (units[m[2]] || 1)) +} + +function parseViewbox(viewbox: string): IAttributes { + const bounds = viewbox.split(' ') + return { + height: parseLength(bounds[3]) as number, + width: parseLength(bounds[2]) as number + } +} + +function parseAttributes(root: string): IAttributes { + const width = root.match(extractorRegExps.width) + const height = root.match(extractorRegExps.height) + const viewbox = root.match(extractorRegExps.viewbox) + return { + height: height && parseLength(height[2]) as number, + viewbox: viewbox && parseViewbox(viewbox[2]) as IAttributes, + width: width && parseLength(width[2]) as number, + } +} + +function calculateByDimensions(attrs: IAttributes): ISize { + return { + height: attrs.height as number, + width: attrs.width as number, + } +} + +function calculateByViewbox(attrs: IAttributes, viewbox: IAttributes): ISize { + const ratio = (viewbox.width as number) / (viewbox.height as number) + if (attrs.width) { + return { + height: Math.floor(attrs.width / ratio), + width: attrs.width, + } + } + if (attrs.height) { + return { + height: attrs.height, + width: Math.floor(attrs.height * ratio), + } + } + return { + height: viewbox.height as number, + width: viewbox.width as number, + } +} + +export const SVG: IImage = { + validate(buffer) { + const str = String(buffer) + return svgReg.test(str) + }, + + calculate(buffer) { + const root = buffer.toString('utf8').match(extractorRegExps.root) + if (root) { + const attrs = parseAttributes(root[0]) + if (attrs.width && attrs.height) { + return calculateByDimensions(attrs) + } + if (attrs.viewbox) { + return calculateByViewbox(attrs, attrs.viewbox) + } + } + throw new TypeError('Invalid SVG') + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/tiff.ts b/packages/astro/src/assets/vendor/image-size/types/tiff.ts new file mode 100644 index 000000000000..1be697d29da4 --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/tiff.ts @@ -0,0 +1,115 @@ +// based on http://www.compix.com/fileformattif.htm +// TO-DO: support big-endian as well +import * as fs from 'fs' +import { readUInt } from '../readUInt.js' +import type { IImage } from './interface' + +// Read IFD (image-file-directory) into a buffer +function readIFD(buffer: Buffer, filepath: string, isBigEndian: boolean) { + + const ifdOffset = readUInt(buffer, 32, 4, isBigEndian) + + // read only till the end of the file + let bufferSize = 1024 + const fileSize = fs.statSync(filepath).size + if (ifdOffset + bufferSize > fileSize) { + bufferSize = fileSize - ifdOffset - 10 + } + + // populate the buffer + const endBuffer = Buffer.alloc(bufferSize) + const descriptor = fs.openSync(filepath, 'r') + fs.readSync(descriptor, endBuffer, 0, bufferSize, ifdOffset) + fs.closeSync(descriptor) + + return endBuffer.slice(2) +} + +// TIFF values seem to be messed up on Big-Endian, this helps +function readValue(buffer: Buffer, isBigEndian: boolean): number { + const low = readUInt(buffer, 16, 8, isBigEndian) + const high = readUInt(buffer, 16, 10, isBigEndian) + return (high << 16) + low +} + +// move to the next tag +function nextTag(buffer: Buffer) { + if (buffer.length > 24) { + return buffer.slice(12) + } +} + +// Extract IFD tags from TIFF metadata +function extractTags(buffer: Buffer, isBigEndian: boolean) { + const tags: {[key: number]: number} = {} + + let temp: Buffer | undefined = buffer + while (temp && temp.length) { + const code = readUInt(temp, 16, 0, isBigEndian) + const type = readUInt(temp, 16, 2, isBigEndian) + const length = readUInt(temp, 32, 4, isBigEndian) + + // 0 means end of IFD + if (code === 0) { + break + } else { + // 256 is width, 257 is height + // if (code === 256 || code === 257) { + if (length === 1 && (type === 3 || type === 4)) { + tags[code] = readValue(temp, isBigEndian) + } + + // move to the next tag + temp = nextTag(temp) + } + } + + return tags +} + +// Test if the TIFF is Big Endian or Little Endian +function determineEndianness(buffer: Buffer) { + const signature = buffer.toString('ascii', 0, 2) + if ('II' === signature) { + return 'LE' + } else if ('MM' === signature) { + return 'BE' + } +} + +const signatures = [ + // '492049', // currently not supported + '49492a00', // Little endian + '4d4d002a', // Big Endian + // '4d4d002a', // BigTIFF > 4GB. currently not supported +] + +export const TIFF: IImage = { + validate(buffer) { + return signatures.includes(buffer.toString('hex', 0, 4)) + }, + + calculate(buffer, filepath) { + if (!filepath) { + throw new TypeError('Tiff doesn\'t support buffer') + } + + // Determine BE/LE + const isBigEndian = determineEndianness(buffer) === 'BE' + + // read the IFD + const ifdBuffer = readIFD(buffer, filepath, isBigEndian) + + // extract the tags from the IFD + const tags = extractTags(ifdBuffer, isBigEndian) + + const width = tags[256] + const height = tags[257] + + if (!width || !height) { + throw new TypeError('Invalid Tiff. Missing tags') + } + + return { height, width } + } +} diff --git a/packages/astro/src/assets/vendor/image-size/types/webp.ts b/packages/astro/src/assets/vendor/image-size/types/webp.ts new file mode 100644 index 000000000000..aaafbf12d82d --- /dev/null +++ b/packages/astro/src/assets/vendor/image-size/types/webp.ts @@ -0,0 +1,65 @@ +// based on https://developers.google.com/speed/webp/docs/riff_container +import type { IImage, ISize } from './interface' + +function calculateExtended(buffer: Buffer): ISize { + return { + height: 1 + buffer.readUIntLE(7, 3), + width: 1 + buffer.readUIntLE(4, 3) + } +} + +function calculateLossless(buffer: Buffer): ISize { + return { + height: 1 + (((buffer[4] & 0xF) << 10) | (buffer[3] << 2) | ((buffer[2] & 0xC0) >> 6)), + width: 1 + (((buffer[2] & 0x3F) << 8) | buffer[1]) + } +} + +function calculateLossy(buffer: Buffer): ISize { + // `& 0x3fff` returns the last 14 bits + // TO-DO: include webp scaling in the calculations + return { + height: buffer.readInt16LE(8) & 0x3fff, + width: buffer.readInt16LE(6) & 0x3fff + } +} + +export const WEBP: IImage = { + validate(buffer) { + const riffHeader = 'RIFF' === buffer.toString('ascii', 0, 4) + const webpHeader = 'WEBP' === buffer.toString('ascii', 8, 12) + const vp8Header = 'VP8' === buffer.toString('ascii', 12, 15) + return (riffHeader && webpHeader && vp8Header) + }, + + calculate(buffer) { + const chunkHeader = buffer.toString('ascii', 12, 16) + buffer = buffer.slice(20, 30) + + // Extended webp stream signature + if (chunkHeader === 'VP8X') { + const extendedHeader = buffer[0] + const validStart = (extendedHeader & 0xc0) === 0 + const validEnd = (extendedHeader & 0x01) === 0 + if (validStart && validEnd) { + return calculateExtended(buffer) + } else { + // TODO: breaking change + throw new TypeError('Invalid WebP') + } + } + + // Lossless webp stream signature + if (chunkHeader === 'VP8 ' && buffer[0] !== 0x2f) { + return calculateLossy(buffer) + } + + // Lossy webp stream signature + const signature = buffer.toString('hex', 3, 6) + if (chunkHeader === 'VP8L' && signature !== '9d012a') { + return calculateLossless(buffer) + } + + throw new TypeError('Invalid WebP') + } +} diff --git a/packages/astro/src/assets/vendor/queue/LICENSE b/packages/astro/src/assets/vendor/queue/LICENSE new file mode 100644 index 000000000000..50e946098625 --- /dev/null +++ b/packages/astro/src/assets/vendor/queue/LICENSE @@ -0,0 +1,8 @@ +The MIT License (MIT) +Copyright (c) 2014 Jesse Tane + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/astro/src/assets/vendor/queue/queue.js b/packages/astro/src/assets/vendor/queue/queue.js new file mode 100644 index 000000000000..6c71704355a4 --- /dev/null +++ b/packages/astro/src/assets/vendor/queue/queue.js @@ -0,0 +1,225 @@ +const has = Object.prototype.hasOwnProperty + +/** + * Since CustomEvent is only supported in nodejs since version 19, + * you have to create your own class instead of using CustomEvent + * @see https://github.com/nodejs/node/issues/40678 + * */ +export class QueueEvent extends Event { + constructor (name, detail) { + super(name) + this.detail = detail + } +} + + +export default class Queue extends EventTarget { + constructor (options = {}) { + super() + const { concurrency = Infinity, timeout = 0, autostart = false, results = null } = options + + this.concurrency = concurrency + this.timeout = timeout + this.autostart = autostart + this.results = results + this.pending = 0 + this.session = 0 + this.running = false + this.jobs = [] + this.timers = [] + + this.addEventListener('error', this._errorHandler) + } + + _errorHandler(evt) { + this.end(evt.detail.error); + } + + pop () { + return this.jobs.pop() + } + + shift () { + return this.jobs.shift() + } + + indexOf (searchElement, fromIndex) { + return this.jobs.indexOf(searchElement, fromIndex) + } + + lastIndexOf (searchElement, fromIndex) { + if (fromIndex !== undefined) { return this.jobs.lastIndexOf(searchElement, fromIndex) } + return this.jobs.lastIndexOf(searchElement) + } + + slice (start, end) { + this.jobs = this.jobs.slice(start, end) + return this + } + + reverse () { + this.jobs.reverse() + return this + } + + push (...workers) { + const methodResult = this.jobs.push(...workers) + if (this.autostart) { + this.start() + } + return methodResult + } + + unshift (...workers) { + const methodResult = this.jobs.unshift(...workers) + if (this.autostart) { + this.start() + } + return methodResult + } + + splice (start, deleteCount, ...workers) { + this.jobs.splice(start, deleteCount, ...workers) + if (this.autostart) { + this.start() + } + return this + } + + get length () { + return this.pending + this.jobs.length + } + + start (callback) { + let awaiter; + + if (callback) { + this._addCallbackToEndEvent(callback) + } else { + awaiter = this._createPromiseToEndEvent(); + } + + this.running = true + + if (this.pending >= this.concurrency) { + return + } + + if (this.jobs.length === 0) { + if (this.pending === 0) { + this.done() + } + return + } + + const job = this.jobs.shift() + const session = this.session + const timeout = (job !== undefined) && has.call(job, 'timeout') ? job.timeout : this.timeout + let once = true + let timeoutId = null + let didTimeout = false + let resultIndex = null + + const next = (error, ...result) => { + if (once && this.session === session) { + once = false + this.pending-- + if (timeoutId !== null) { + this.timers = this.timers.filter((tID) => tID !== timeoutId) + clearTimeout(timeoutId) + } + + if (error) { + this.dispatchEvent(new QueueEvent('error', { error, job })) + } else if (!didTimeout) { + if (resultIndex !== null && this.results !== null) { + this.results[resultIndex] = [...result] + } + this.dispatchEvent(new QueueEvent('success', { result: [...result], job })) + } + + if (this.session === session) { + if (this.pending === 0 && this.jobs.length === 0) { + this.done() + } else if (this.running) { + this.start() + } + } + } + } + + if (timeout) { + timeoutId = setTimeout(() => { + didTimeout = true + this.dispatchEvent(new QueueEvent('timeout', { next, job })) + next() + }, timeout) + this.timers.push(timeoutId) + } + + if (this.results != null) { + resultIndex = this.results.length + this.results[resultIndex] = null + } + + this.pending++ + this.dispatchEvent(new QueueEvent('start', { job })) + + const promise = job(next) + + if (promise !== undefined && typeof promise.then === 'function') { + promise.then(function (result) { + return next(undefined, result) + }).catch(function (err) { + return next(err || true) + }) + } + + if (this.running && this.jobs.length > 0) { + return this.start() + } + + return awaiter; + } + + stop () { + this.running = false + } + + end (error) { + this.clearTimers() + this.jobs.length = 0 + this.pending = 0 + this.done(error) + } + + clearTimers () { + this.timers.forEach((timer) => { + clearTimeout(timer) + }) + + this.timers = [] + } + + _addCallbackToEndEvent (cb) { + const onend = (evt) => { + this.removeEventListener('end', onend) + cb(evt.detail.error, this.results) + } + this.addEventListener('end', onend) + } + + _createPromiseToEndEvent() { + return new Promise((resolve) => { + this._addCallbackToEndEvent((error, results) => { + resolve({ error, results }); + }); + }); + } + + done (error) { + this.session++ + this.running = false + this.dispatchEvent(new QueueEvent('end', { error })) + } +} diff --git a/packages/astro/src/core/sync/index.ts b/packages/astro/src/core/sync/index.ts index 82575f0837a6..826da6c0c477 100644 --- a/packages/astro/src/core/sync/index.ts +++ b/packages/astro/src/core/sync/index.ts @@ -67,7 +67,7 @@ export async function sync( { server: { middlewareMode: true, hmr: false }, optimizeDeps: { entries: [] }, - ssr: { external: ['image-size'] }, + ssr: { external: [] }, logLevel: 'silent', }, { settings, logging, mode: 'build', command: 'build', fs } diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index 62cd8a58e778..3ef4a5f6a4e2 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -13,6 +13,7 @@ import type { Plugin } from 'vite'; import { normalizePath } from 'vite'; import type { AstroSettings } from '../@types/astro'; import { imageMetadata } from '../assets/index.js'; +import imageSize from '../assets/vendor/image-size/index.js'; import { AstroError, AstroErrorData, MarkdownError } from '../core/errors/index.js'; import type { LogOptions } from '../core/logger/core.js'; import { warn } from '../core/logger/core.js'; @@ -104,6 +105,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu imageService, assetsDir: new URL('./assets/', settings.config.srcDir), resolveImage: this.meta.watchMode ? undefined : resolveImage.bind(this, fileId), + getImageMetadata: imageSize, }); this; diff --git a/packages/markdown/remark/package.json b/packages/markdown/remark/package.json index 1dda55cc7d96..38d55f07936f 100644 --- a/packages/markdown/remark/package.json +++ b/packages/markdown/remark/package.json @@ -30,7 +30,6 @@ "dependencies": { "@astrojs/prism": "^2.1.0", "github-slugger": "^1.4.0", - "image-size": "^1.0.2", "import-meta-resolve": "^2.1.0", "rehype-raw": "^6.1.1", "rehype-stringify": "^9.0.3", diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index 5c5dcdc66174..ffae6be4f73e 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -116,7 +116,7 @@ export async function renderMarkdown( }); if (opts.experimentalAssets) { - parser.use(rehypeImages(await opts.imageService, opts.assetsDir)); + parser.use(rehypeImages(await opts.imageService, opts.assetsDir, opts.getImageMetadata)); } if (!isPerformanceBenchmark) { parser.use([rehypeHeadingIds]); diff --git a/packages/markdown/remark/src/rehype-images.ts b/packages/markdown/remark/src/rehype-images.ts index f94960ba0b74..fcd39cd6015a 100644 --- a/packages/markdown/remark/src/rehype-images.ts +++ b/packages/markdown/remark/src/rehype-images.ts @@ -1,11 +1,10 @@ -import sizeOf from 'image-size'; import { join as pathJoin } from 'node:path'; import { fileURLToPath } from 'node:url'; import { visit } from 'unist-util-visit'; import { pathToFileURL } from 'url'; -import type { MarkdownVFile } from './types.js'; +import type { ImageMetadata, MarkdownVFile } from './types.js'; -export function rehypeImages(imageService: any, assetsDir: URL | undefined) { +export function rehypeImages(imageService: any, assetsDir: URL | undefined, getImageMetadata: any) { return () => function (tree: any, file: MarkdownVFile) { visit(tree, (node) => { @@ -24,10 +23,10 @@ export function rehypeImages(imageService: any, assetsDir: URL | undefined) { fileURL = pathToFileURL(pathJoin(file.dirname, node.properties.src)); } - const fileData = sizeOf(fileURLToPath(fileURL)); - fileURL.searchParams.append('origWidth', fileData.width!.toString()); - fileURL.searchParams.append('origHeight', fileData.height!.toString()); - fileURL.searchParams.append('origFormat', fileData.type!.toString()); + const fileData = getImageMetadata!(fileURLToPath(fileURL)) as ImageMetadata; + fileURL.searchParams.append('origWidth', fileData.width.toString()); + fileURL.searchParams.append('origHeight', fileData.height.toString()); + fileURL.searchParams.append('origFormat', fileData.type.toString()); let options = { src: { diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts index 38fe9fc74bbd..1b88f2442519 100644 --- a/packages/markdown/remark/src/types.ts +++ b/packages/markdown/remark/src/types.ts @@ -51,6 +51,13 @@ export interface AstroMarkdownOptions { smartypants?: boolean; } +export interface ImageMetadata { + src: string; + width: number; + height: number; + type: string; +} + export interface MarkdownRenderingOptions extends AstroMarkdownOptions { /** @internal */ fileURL?: URL; @@ -64,6 +71,7 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions { imageService?: any; assetsDir?: URL; resolveImage?: (path: string) => Promise; + getImageMetadata?: any; } export interface MarkdownHeading { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44e6f54679da..5c460b4b7544 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -480,7 +480,6 @@ importers: github-slugger: ^2.0.0 gray-matter: ^4.0.3 html-escaper: ^3.0.3 - image-size: ^1.0.2 kleur: ^4.1.4 magic-string: ^0.27.0 memfs: ^3.4.7 @@ -548,7 +547,6 @@ importers: github-slugger: 2.0.0 gray-matter: 4.0.3 html-escaper: 3.0.3 - image-size: 1.0.2 kleur: 4.1.5 magic-string: 0.27.0 mime: 3.0.0 @@ -3806,7 +3804,6 @@ importers: astro-scripts: workspace:* chai: ^4.3.6 github-slugger: ^1.4.0 - image-size: ^1.0.2 import-meta-resolve: ^2.1.0 mdast-util-mdx-expression: ^1.3.1 mocha: ^9.2.2 @@ -3823,7 +3820,6 @@ importers: dependencies: '@astrojs/prism': link:../../astro-prism github-slugger: 1.5.0 - image-size: 1.0.2 import-meta-resolve: 2.2.1 rehype-raw: 6.1.1 rehype-stringify: 9.0.3