Skip to content

Commit

Permalink
Vendor image-size (#6559)
Browse files Browse the repository at this point in the history
* feat(assets): Vendor image-size

* fix(assets): Also vendor queue, the CJS virus runs deep

* fix: remove unneeded queue

* chore: lockfile

* fix: build

* fix: build part 2

* chore: changeset
  • Loading branch information
Princesseuh authored Mar 16, 2023
1 parent 67d3d1d commit 90e5f87
Show file tree
Hide file tree
Showing 35 changed files with 1,422 additions and 23 deletions.
6 changes: 6 additions & 0 deletions .changeset/shy-walls-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'astro': patch
'@astrojs/markdown-remark': patch
---

Vendor `image-size` to fix CJS-related issues
7 changes: 3 additions & 4 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
7 changes: 2 additions & 5 deletions packages/astro/src/assets/utils/metadata.ts
Original file line number Diff line number Diff line change
@@ -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<Metadata | undefined> {
if (!sizeOf) {
sizeOf = await import('image-size').then((mod) => mod.default);
}
let file = data;
if (!file) {
try {
Expand All @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/assets/vendor/README.md
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions packages/astro/src/assets/vendor/image-size/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 30 additions & 0 deletions packages/astro/src/assets/vendor/image-size/detector.ts
Original file line number Diff line number Diff line change
@@ -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)
}
146 changes: 146 additions & 0 deletions packages/astro/src/assets/vendor/image-size/index.ts
Original file line number Diff line number Diff line change
@@ -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<Buffer>}
*/
async function asyncFileToBuffer(filepath: string): Promise<Buffer> {
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);
10 changes: 10 additions & 0 deletions packages/astro/src/assets/vendor/image-size/readUInt.ts
Original file line number Diff line number Diff line change
@@ -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)
}
38 changes: 38 additions & 0 deletions packages/astro/src/assets/vendor/image-size/types.ts
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions packages/astro/src/assets/vendor/image-size/types/bmp.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
16 changes: 16 additions & 0 deletions packages/astro/src/assets/vendor/image-size/types/cur.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
14 changes: 14 additions & 0 deletions packages/astro/src/assets/vendor/image-size/types/dds.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
16 changes: 16 additions & 0 deletions packages/astro/src/assets/vendor/image-size/types/gif.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Loading

0 comments on commit 90e5f87

Please sign in to comment.