Skip to content

Commit fa86b90

Browse files
committed
feat: fix/improve compile
1 parent 260c122 commit fa86b90

27 files changed

+590
-197
lines changed

packages/astro/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@
132132
"cookie": "^1.1.1",
133133
"cssesc": "^3.0.0",
134134
"debug": "^4.4.3",
135-
"deterministic-object-hash": "^2.0.2",
136135
"devalue": "^5.6.2",
137136
"diff": "^8.0.3",
138137
"dlv": "^1.1.3",

packages/astro/src/assets/build/generate.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import fs, { readFileSync } from 'node:fs';
22
import { basename } from 'node:path/posix';
33
import colors from 'piccolore';
4-
import type { BuildApp } from '../../core/build/app.js';
54
import { getOutDirWithinCwd } from '../../core/build/common.js';
5+
import type { StaticBuildOptions } from '../../core/build/types.js';
66
import { getTimeStat } from '../../core/build/util.js';
77
import { AstroError } from '../../core/errors/errors.js';
88
import { AstroErrorData } from '../../core/errors/index.js';
@@ -50,14 +50,12 @@ type ImageData = {
5050
};
5151

5252
export async function prepareAssetsGenerationEnv(
53-
app: BuildApp,
53+
options: StaticBuildOptions,
5454
totalCount: number,
5555
): Promise<AssetEnv> {
56-
const settings = app.getSettings();
57-
const logger = app.logger;
58-
const manifest = app.getManifest();
56+
const { settings, logger } = options;
5957
let useCache = true;
60-
const assetsCacheDir = new URL('assets/', app.manifest.cacheDir);
58+
const assetsCacheDir = new URL('assets/', settings.config.cacheDir);
6159
const count = { total: totalCount, current: 1 };
6260

6361
// Ensure that the cache directory exists
@@ -75,11 +73,11 @@ export async function prepareAssetsGenerationEnv(
7573
let serverRoot: URL, clientRoot: URL;
7674
if (isServerOutput) {
7775
// Images are collected during prerender, which outputs to .prerender/ subdirectory
78-
serverRoot = new URL('.prerender/', manifest.buildServerDir);
79-
clientRoot = manifest.buildClientDir;
76+
serverRoot = new URL('.prerender/', settings.config.build.server);
77+
clientRoot = settings.config.build.client;
8078
} else {
81-
serverRoot = getOutDirWithinCwd(manifest.outDir);
82-
clientRoot = manifest.outDir;
79+
serverRoot = getOutDirWithinCwd(settings.config.outDir);
80+
clientRoot = settings.config.outDir;
8381
}
8482

8583
return {
@@ -91,7 +89,7 @@ export async function prepareAssetsGenerationEnv(
9189
serverRoot,
9290
clientRoot,
9391
imageConfig: settings.config.image,
94-
assetsFolder: manifest.assetsDir,
92+
assetsFolder: settings.config.build.assets,
9593
};
9694
}
9795

packages/astro/src/assets/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { getConfiguredImageService, getImage } from './internal.js';
22
export { baseService, isLocalService } from './services/service.js';
3+
export { hashTransform, propsToFilename } from './utils/hash.js';
34
export { type LocalImageProps, type RemoteImageProps } from './types.js';
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Vendored from deterministic-object-hash@2.0.2 (MIT)
3+
* https://github.com/nicholasgasior/deterministic-object-hash
4+
*
5+
* Only `deterministicString` is needed - the async `deterministicHash` (which
6+
* pulls in `node:crypto`) is intentionally excluded so this module stays
7+
* runtime-agnostic (works in Node, workerd, browsers, etc.).
8+
*/
9+
10+
const objConstructorString = Function.prototype.toString.call(Object);
11+
12+
function isPlainObject(value: unknown): value is Record<string | symbol, unknown> {
13+
if (
14+
typeof value !== 'object' ||
15+
value === null ||
16+
Object.prototype.toString.call(value) !== '[object Object]'
17+
) {
18+
return false;
19+
}
20+
const proto = Object.getPrototypeOf(value);
21+
if (proto === null) {
22+
return true;
23+
}
24+
if (!Object.prototype.hasOwnProperty.call(proto, 'constructor')) {
25+
return false;
26+
}
27+
return (
28+
typeof proto.constructor === 'function' &&
29+
proto.constructor instanceof proto.constructor &&
30+
Function.prototype.toString.call(proto.constructor) === objConstructorString
31+
);
32+
}
33+
34+
/** Recursively serializes any JS value into a deterministic string. */
35+
export function deterministicString(input: unknown): string {
36+
if (typeof input === 'string') {
37+
return JSON.stringify(input);
38+
} else if (typeof input === 'symbol' || typeof input === 'function') {
39+
return input.toString();
40+
} else if (typeof input === 'bigint') {
41+
return `${input}n`;
42+
} else if (
43+
input === globalThis ||
44+
input === undefined ||
45+
input === null ||
46+
typeof input === 'boolean' ||
47+
typeof input === 'number' ||
48+
typeof input !== 'object'
49+
) {
50+
return `${input}`;
51+
} else if (input instanceof Date) {
52+
return `(${input.constructor.name}:${input.getTime()})`;
53+
} else if (
54+
input instanceof RegExp ||
55+
input instanceof Error ||
56+
input instanceof WeakMap ||
57+
input instanceof WeakSet
58+
) {
59+
return `(${input.constructor.name}:${input.toString()})`;
60+
} else if (input instanceof Set) {
61+
let ret = `(${input.constructor.name}:[`;
62+
for (const val of input.values()) {
63+
ret += `${deterministicString(val)},`;
64+
}
65+
ret += '])';
66+
return ret;
67+
} else if (
68+
Array.isArray(input) ||
69+
input instanceof Int8Array ||
70+
input instanceof Uint8Array ||
71+
input instanceof Uint8ClampedArray ||
72+
input instanceof Int16Array ||
73+
input instanceof Uint16Array ||
74+
input instanceof Int32Array ||
75+
input instanceof Uint32Array ||
76+
input instanceof Float32Array ||
77+
input instanceof Float64Array ||
78+
input instanceof BigInt64Array ||
79+
input instanceof BigUint64Array
80+
) {
81+
let ret = `(${input.constructor.name}:[`;
82+
for (const [k, v] of input.entries()) {
83+
ret += `(${k}:${deterministicString(v)}),`;
84+
}
85+
ret += '])';
86+
return ret;
87+
} else if (input instanceof ArrayBuffer || input instanceof SharedArrayBuffer) {
88+
if (input.byteLength % 8 === 0) {
89+
return deterministicString(new BigUint64Array(input));
90+
} else if (input.byteLength % 4 === 0) {
91+
return deterministicString(new Uint32Array(input));
92+
} else if (input.byteLength % 2 === 0) {
93+
return deterministicString(new Uint16Array(input));
94+
} else {
95+
let ret = '(';
96+
for (let i = 0; i < input.byteLength; i++) {
97+
ret += `${deterministicString(new Uint8Array(input.slice(i, i + 1)))},`;
98+
}
99+
ret += ')';
100+
return ret;
101+
}
102+
} else if (input instanceof Map || isPlainObject(input)) {
103+
const sortable: [string, string][] = [];
104+
const entries = input instanceof Map ? input.entries() : Object.entries(input);
105+
for (const [k, v] of entries) {
106+
sortable.push([deterministicString(k), deterministicString(v)]);
107+
}
108+
if (!(input instanceof Map)) {
109+
const symbolKeys = Object.getOwnPropertySymbols(input);
110+
for (let i = 0; i < symbolKeys.length; i++) {
111+
sortable.push([
112+
deterministicString(symbolKeys[i]!),
113+
deterministicString((input as Record<symbol, unknown>)[symbolKeys[i]!]),
114+
]);
115+
}
116+
}
117+
sortable.sort(([a], [b]) => a.localeCompare(b));
118+
let ret = `(${input.constructor.name}:[`;
119+
for (const [k, v] of sortable) {
120+
ret += `(${k}:${v}),`;
121+
}
122+
ret += '])';
123+
return ret;
124+
}
125+
126+
const allEntries: [string, string][] = [];
127+
for (const k in input) {
128+
allEntries.push([
129+
deterministicString(k),
130+
deterministicString((input as Record<string, unknown>)[k]),
131+
]);
132+
}
133+
const symbolKeys = Object.getOwnPropertySymbols(input);
134+
for (let i = 0; i < symbolKeys.length; i++) {
135+
allEntries.push([
136+
deterministicString(symbolKeys[i]!),
137+
deterministicString((input as Record<symbol, unknown>)[symbolKeys[i]!]),
138+
]);
139+
}
140+
allEntries.sort(([a], [b]) => a.localeCompare(b));
141+
let ret = `(${input.constructor.name}:[`;
142+
for (const [k, v] of allEntries) {
143+
ret += `(${k}:${v}),`;
144+
}
145+
ret += '])';
146+
return ret;
147+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { deterministicString } from './deterministic-string.js';
2+
import { removeQueryString } from '@astrojs/internal-helpers/path';
3+
import { shorthash } from '../../runtime/server/shorthash.js';
4+
import type { ImageTransform } from '../types.js';
5+
import { isESMImportedImage } from './imageKind.js';
6+
7+
// Taken from https://github.com/rollup/rollup/blob/a8647dac0fe46c86183be8596ef7de25bc5b4e4b/src/utils/sanitizeFileName.ts
8+
// eslint-disable-next-line no-control-regex
9+
const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$%&*+,:;<=>?[\]^`{|}\u007F]/g;
10+
11+
/** Pure-string replacement for `path.posix.basename` (no node: import). */
12+
function basename(filePath: string, ext?: string): string {
13+
let end = filePath.length;
14+
while (end > 0 && filePath[end - 1] === '/') end--;
15+
const stripped = filePath.slice(0, end);
16+
17+
const lastSlash = stripped.lastIndexOf('/');
18+
const base = lastSlash === -1 ? stripped : stripped.slice(lastSlash + 1);
19+
20+
if (ext && base.endsWith(ext)) {
21+
return base.slice(0, base.length - ext.length);
22+
}
23+
return base;
24+
}
25+
26+
/** Pure-string replacement for `path.posix.dirname` (no node: import). */
27+
function dirname(filePath: string): string {
28+
const lastSlash = filePath.lastIndexOf('/');
29+
if (lastSlash === -1) return '.';
30+
if (lastSlash === 0) return '/';
31+
return filePath.slice(0, lastSlash);
32+
}
33+
34+
/** Pure-string replacement for `path.posix.extname` (no node: import). */
35+
function extname(filePath: string): string {
36+
const base = basename(filePath);
37+
const dotIndex = base.lastIndexOf('.');
38+
if (dotIndex <= 0) return '';
39+
return base.slice(dotIndex);
40+
}
41+
42+
/**
43+
* Converts a file path and transformation properties into a formatted filename.
44+
*
45+
* `<prefixDirname>/<baseFilename>_<hash><outputExtension>`
46+
*/
47+
export function propsToFilename(filePath: string, transform: ImageTransform, hash: string): string {
48+
let filename = decodeURIComponent(removeQueryString(filePath));
49+
const ext = extname(filename);
50+
if (filePath.startsWith('data:')) {
51+
filename = shorthash(filePath);
52+
} else {
53+
filename = basename(filename, ext).replace(INVALID_CHAR_REGEX, '_');
54+
}
55+
const prefixDirname = isESMImportedImage(transform.src) ? dirname(filePath) : '';
56+
57+
let outputExt = transform.format ? `.${transform.format}` : ext;
58+
return `${prefixDirname}/${filename}_${hash}${outputExt}`;
59+
}
60+
61+
/** Hashes the subset of transform properties that affect the output image. */
62+
export function hashTransform(
63+
transform: ImageTransform,
64+
imageService: string,
65+
propertiesToHash: string[],
66+
): string {
67+
const hashFields = propertiesToHash.reduce(
68+
(acc, prop) => {
69+
acc[prop] = transform[prop];
70+
return acc;
71+
},
72+
{ imageService } as Record<string, unknown>,
73+
);
74+
return shorthash(deterministicString(hashFields));
75+
}

packages/astro/src/assets/utils/node.ts

Lines changed: 5 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import fs from 'node:fs/promises';
2-
import path, { basename, dirname, extname } from 'node:path';
2+
import path from 'node:path';
33
import { fileURLToPath, pathToFileURL } from 'node:url';
4-
import { deterministicString } from 'deterministic-object-hash';
54
import type * as vite from 'vite';
65
import { generateContentHash } from '../../core/encryption.js';
7-
import { prependForwardSlash, removeQueryString, slash } from '../../core/path.js';
8-
import { shorthash } from '../../runtime/server/shorthash.js';
9-
import type { ImageMetadata, ImageTransform } from '../types.js';
10-
import { isESMImportedImage } from './imageKind.js';
6+
import { prependForwardSlash, slash } from '../../core/path.js';
7+
import type { ImageMetadata } from '../types.js';
118
import { imageMetadata } from './metadata.js';
129

10+
export { hashTransform, propsToFilename } from './hash.js';
11+
1312
type FileEmitter = vite.Rollup.EmitFile;
1413
type ImageMetadataWithContents = ImageMetadata & { contents?: Buffer };
1514

@@ -142,70 +141,3 @@ function fileURLToNormalizedPath(filePath: URL): string {
142141
// Uses `slash` instead of Vite's `normalizePath` to avoid CJS bundling issues.
143142
return slash(fileURLToPath(filePath) + filePath.search).replace(/\\/g, '/');
144143
}
145-
146-
// Taken from https://github.com/rollup/rollup/blob/a8647dac0fe46c86183be8596ef7de25bc5b4e4b/src/utils/sanitizeFileName.ts
147-
// eslint-disable-next-line no-control-regex
148-
const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$%&*+,:;<=>?[\]^`{|}\u007F]/g;
149-
150-
/**
151-
* Converts a file path and transformation properties of the transformation image service, into a formatted filename.
152-
*
153-
* The formatted filename follows this structure:
154-
*
155-
* `<prefixDirname>/<baseFilename>_<hash><outputExtension>`
156-
*
157-
* - `prefixDirname`: If the image is an ESM imported image, this is the directory name of the original file path; otherwise, it will be an empty string.
158-
* - `baseFilename`: The base name of the file or a hashed short name if the file is a `data:` URI.
159-
* - `hash`: A unique hash string generated to distinguish the transformed file.
160-
* - `outputExtension`: The desired output file extension derived from the `transform.format` or the original file extension.
161-
*
162-
* ## Example
163-
* - Input: `filePath = '/images/photo.jpg'`, `transform = { format: 'png', src: '/images/photo.jpg' }`, `hash = 'abcd1234'`.
164-
* - Output: `/images/photo_abcd1234.png`
165-
*
166-
* @param {string} filePath - The original file path or data URI of the source image.
167-
* @param {ImageTransform} transform - An object representing the transformation properties, including format and source.
168-
* @param {string} hash - A unique hash used to differentiate the transformed file.
169-
* @return {string} The generated filename based on the provided input, transformations, and hash.
170-
*/
171-
172-
export function propsToFilename(filePath: string, transform: ImageTransform, hash: string): string {
173-
let filename = decodeURIComponent(removeQueryString(filePath));
174-
const ext = extname(filename);
175-
if (filePath.startsWith('data:')) {
176-
filename = shorthash(filePath);
177-
} else {
178-
filename = basename(filename, ext).replace(INVALID_CHAR_REGEX, '_');
179-
}
180-
const prefixDirname = isESMImportedImage(transform.src) ? dirname(filePath) : '';
181-
182-
let outputExt = transform.format ? `.${transform.format}` : ext;
183-
return `${prefixDirname}/${filename}_${hash}${outputExt}`;
184-
}
185-
186-
/**
187-
* Transforms the provided `transform` object into a hash string based on selected properties
188-
* and the specified `imageService`.
189-
*
190-
* @param {ImageTransform} transform - The transform object containing various image transformation properties.
191-
* @param {string} imageService - The name of the image service related to the transform.
192-
* @param {string[]} propertiesToHash - An array of property names from the `transform` object that should be used to generate the hash.
193-
* @return {string} A hashed string created from the specified properties of the `transform` object and the image service.
194-
*/
195-
export function hashTransform(
196-
transform: ImageTransform,
197-
imageService: string,
198-
propertiesToHash: string[],
199-
): string {
200-
// Extract the fields we want to hash
201-
const hashFields = propertiesToHash.reduce(
202-
(acc, prop) => {
203-
// It's possible for `transform[prop]` here to be undefined, or null, but that's fine because it's still consistent
204-
// between different transforms. (ex: every transform without a height will explicitly have a `height: undefined` property)
205-
acc[prop] = transform[prop];
206-
return acc;
207-
},
208-
{ imageService } as Record<string, unknown>,
209-
);
210-
return shorthash(deterministicString(hashFields));
211-
}

packages/astro/src/assets/vite-plugin-assets.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import type { ImageTransform } from './types.js';
2929
import { getAssetsPrefix } from './utils/getAssetsPrefix.js';
3030
import { isESMImportedImage } from './utils/index.js';
3131
import { emitClientAsset } from './utils/assets.js';
32-
import { emitImageMetadata, hashTransform, propsToFilename } from './utils/node.js';
32+
import { hashTransform, propsToFilename } from './utils/hash.js';
33+
import { emitImageMetadata } from './utils/node.js';
3334
import { getProxyCode } from './utils/proxy.js';
3435
import { makeSvgComponent } from './utils/svg.js';
3536
import { createPlaceholderURL, stringifyPlaceholderURL } from './utils/url.js';

0 commit comments

Comments
 (0)