cloudflare-next-intl 0.8.53 → 0.8.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/bin/image_optimizer.mjs +12 -0
- package/dist/src/image_optimizer/blur_svg.d.ts +1 -0
- package/dist/src/image_optimizer/blur_svg.js +16 -0
- package/dist/src/image_optimizer/cache.d.ts +10 -0
- package/dist/src/image_optimizer/cache.js +34 -0
- package/dist/src/image_optimizer/index.d.ts +8 -0
- package/dist/src/image_optimizer/index.js +7 -0
- package/dist/src/image_optimizer/manifest.d.ts +3 -0
- package/dist/src/image_optimizer/manifest.js +25 -0
- package/dist/src/image_optimizer/next_image_shim.d.ts +9 -0
- package/dist/src/image_optimizer/next_image_shim.js +38 -0
- package/dist/src/image_optimizer/plugin.d.ts +8 -0
- package/dist/src/image_optimizer/plugin.js +64 -0
- package/dist/src/image_optimizer/process_image.d.ts +12 -0
- package/dist/src/image_optimizer/process_image.js +89 -0
- package/dist/src/image_optimizer/run.d.ts +3 -0
- package/dist/src/image_optimizer/run.js +73 -0
- package/dist/src/image_optimizer/test_helpers.d.ts +3 -0
- package/dist/src/image_optimizer/test_helpers.js +25 -0
- package/dist/src/image_optimizer/types.d.ts +88 -0
- package/dist/src/image_optimizer/types.js +82 -0
- package/dist/src/vite/index.d.ts +1 -0
- package/dist/src/vite/index.js +1 -0
- package/dist/src/vite/plugin.d.ts +8 -0
- package/dist/src/vite/plugin.js +6 -0
- package/llms.txt +2 -1
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -256,10 +256,11 @@ export default defineConfig({
|
|
|
256
256
|
```
|
|
257
257
|
|
|
258
258
|
##### What `cloudflareNextIntl()` Does
|
|
259
|
-
1. **
|
|
260
|
-
2. **
|
|
261
|
-
3. **
|
|
262
|
-
4. **
|
|
259
|
+
1. **Build-Time & Dev Image Optimizer (`imageOptimizer`)**: Automatically scans your image directories (`public/images`, `public/icons`), downscales oversized assets, produces modern `.avif` and `.webp` formats, generates 8px `.blur.webp` thumbnails with Next.js-matching SVG Gaussian blur placeholders, and provides transparent `<Image placeholder="blur" />` shimming via virtual modules.
|
|
260
|
+
2. **Locale File Bundling & Resolution (`localeFiles`)**: Resolves `@locale-file/*` to your `./messages` directory and transforms dynamic imports into `import.meta.glob('/messages/*.json', { eager: true })` for lightning-fast locale loading on Cloudflare Workers.
|
|
261
|
+
3. **User-Agent Stub (`userAgentStub`)**: Prevents Next.js `user-agent` from importing `node:fs` during workerd runtime execution (which otherwise causes runtime 404 / 500 crashes in Workers proxy/middleware).
|
|
262
|
+
4. **Cloudflare Workers Client Stub (`cfWorkersClientStub`)**: Stubs `cloudflare:workers` in client builds so shared modules can be referenced without client bundling errors.
|
|
263
|
+
5. **Build ID Asset Emission (`buildIdAsset`)**: Emits `BUILD_ID` static asset in the client build directory from `process.env.__VINEXT_SHARED_BUILD_ID` or `process.env.__VINEXT_BUILD_ID`.
|
|
263
264
|
|
|
264
265
|
##### Plugin Options
|
|
265
266
|
All features are enabled by default, and can be individually configured or toggled off:
|
|
@@ -271,6 +272,16 @@ import { cloudflareNextIntl } from "cloudflare-next-intl/vite";
|
|
|
271
272
|
export default defineConfig({
|
|
272
273
|
plugins: [
|
|
273
274
|
cloudflareNextIntl({
|
|
275
|
+
imageOptimizer: { // Image optimizer configuration (or `false` to disable)
|
|
276
|
+
maxWidth: 1920, // Downscale max width limit (default: 1920, or `false`)
|
|
277
|
+
formats: ["avif", "webp"], // Target sibling formats (default: ["avif", "webp"], or `false`)
|
|
278
|
+
quality: 80, // Compression quality (default: 80)
|
|
279
|
+
blur: { quality: 70, stdDeviation: 20 }, // Next.js blur placeholder options (or `false`)
|
|
280
|
+
overrides: { // Per-image overrides keyed by public src path
|
|
281
|
+
"/images/hero.png": { maxWidth: false, formats: ["webp"], blur: { quality: 80 } },
|
|
282
|
+
"/images/logo.png": { formats: false, blur: false },
|
|
283
|
+
},
|
|
284
|
+
},
|
|
274
285
|
messagesDir: "./messages", // Path to locale JSON files (default: './messages')
|
|
275
286
|
intlConfigPath: "./src/l18n/intl_config.ts", // Path to intl config (auto-detected if omitted)
|
|
276
287
|
buildIdAsset: true, // Emit BUILD_ID asset (or custom string filename, default: true)
|
|
@@ -283,7 +294,7 @@ export default defineConfig({
|
|
|
283
294
|
```
|
|
284
295
|
|
|
285
296
|
Individual standalone plugins are also exported if you only need a specific feature:
|
|
286
|
-
`buildIdAsset`, `localeFilePlugin`, `userAgentStubPlugin`, `cfWorkersClientStubPlugin`.
|
|
297
|
+
`imageOptimizerPlugin` (or `imageOptimizer`), `buildIdAsset`, `localeFilePlugin`, `userAgentStubPlugin`, `cfWorkersClientStubPlugin`.
|
|
287
298
|
|
|
288
299
|
```tsx
|
|
289
300
|
// Client Components ("use client")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { run } from "../dist/src/image_optimizer/run.js";
|
|
4
|
+
import { resolveOptions } from "../dist/src/image_optimizer/types.js";
|
|
5
|
+
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const options = resolveOptions();
|
|
8
|
+
const cacheFile = path.resolve(root, options.cacheDir, "manifest.json");
|
|
9
|
+
|
|
10
|
+
console.log("[cfni-image-optimizer] scanning images in", options.dirs.join(", "));
|
|
11
|
+
const entries = await run(root, options, cacheFile);
|
|
12
|
+
console.log(`[cfni-image-optimizer] processed ${entries.length} images into ${options.outDir}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getImageBlurSvg(blurDataURL: string, blurWidth?: number, blurHeight?: number, objectFit?: string, stdDeviation?: number): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function getImageBlurSvg(blurDataURL, blurWidth, blurHeight, objectFit, stdDeviation = 20) {
|
|
2
|
+
const std = stdDeviation;
|
|
3
|
+
const viewBox = blurWidth && blurHeight
|
|
4
|
+
? `viewBox='0 0 ${blurWidth * 40} ${blurHeight * 40}'`
|
|
5
|
+
: "";
|
|
6
|
+
const preserveAspectRatio = viewBox
|
|
7
|
+
? "none"
|
|
8
|
+
: objectFit === "contain"
|
|
9
|
+
? "xMidYMid"
|
|
10
|
+
: objectFit === "cover"
|
|
11
|
+
? "xMidYMid slice"
|
|
12
|
+
: "none";
|
|
13
|
+
const svg = `<svg xmlns='http://www.w3.org/2000/svg' ${viewBox}><filter id='b' color-interpolation-filters='sRGB'><feGaussianBlur stdDeviation='${std}'/><feColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/><feFlood x='0' y='0' width='100%' height='100%'/><feComposite operator='out' in='s'/><feComposite in2='SourceGraphic'/><feGaussianBlur stdDeviation='${std}'/></filter><image width='100%' height='100%' x='0' y='0' preserveAspectRatio='${preserveAspectRatio}' style='filter: url(#b);' href='${blurDataURL}'/></svg>`;
|
|
14
|
+
const base64 = Buffer.from(svg).toString("base64");
|
|
15
|
+
return `data:image/svg+xml;base64,${base64}`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { OptimizedImage } from "./types.js";
|
|
2
|
+
export interface CacheEntry {
|
|
3
|
+
mtimeMs: number;
|
|
4
|
+
size: number;
|
|
5
|
+
result: OptimizedImage;
|
|
6
|
+
}
|
|
7
|
+
export type CacheData = Record<string, CacheEntry>;
|
|
8
|
+
export declare function isFresh(sourcePath: string, cached: CacheEntry | undefined, targets: string[]): Promise<boolean>;
|
|
9
|
+
export declare function loadCache(cacheFile: string): Promise<CacheData>;
|
|
10
|
+
export declare function saveCache(cacheFile: string, data: CacheData): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export async function isFresh(sourcePath, cached, targets) {
|
|
5
|
+
if (!cached)
|
|
6
|
+
return false;
|
|
7
|
+
try {
|
|
8
|
+
const stats = await stat(sourcePath);
|
|
9
|
+
if (stats.mtimeMs !== cached.mtimeMs || stats.size !== cached.size) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
for (const target of targets) {
|
|
17
|
+
if (!existsSync(target))
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
export async function loadCache(cacheFile) {
|
|
23
|
+
try {
|
|
24
|
+
const content = await readFile(cacheFile, "utf8");
|
|
25
|
+
return JSON.parse(content);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function saveCache(cacheFile, data) {
|
|
32
|
+
await mkdir(path.dirname(cacheFile), { recursive: true });
|
|
33
|
+
await writeFile(cacheFile, JSON.stringify(data, null, 2), "utf8");
|
|
34
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { imageOptimizerPlugin, imageOptimizer, VIRTUAL_IMAGE_SHIM_ID, default, } from "./plugin.js";
|
|
2
|
+
export { resolveOptions, resolveImageConfig, resolveBlurOptions, DEFAULT_OPTIONS, DEFAULT_BLUR_OPTIONS, SUPPORTED_EXTENSIONS, type ImageFormat, type ImageBlurOptions, type ImageOverrideOptions, type ImageOptimizerPluginOptions, type ResolvedBlurOptions, type ResolvedOptions, type ResolvedImageConfig, type OptimizedImage, type ManifestData, } from "./types.js";
|
|
3
|
+
export { processImage, makeBlurDataURL, toGeneratedPath, toPublicSrc, } from "./process_image.js";
|
|
4
|
+
export { renderManifest, writeManifest, } from "./manifest.js";
|
|
5
|
+
export { isFresh, loadCache, saveCache, type CacheEntry, type CacheData, } from "./cache.js";
|
|
6
|
+
export { collectImages, run, } from "./run.js";
|
|
7
|
+
export { getImageBlurSvg, } from "./blur_svg.js";
|
|
8
|
+
export { type ManifestEntry, } from "./next_image_shim.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { imageOptimizerPlugin, imageOptimizer, VIRTUAL_IMAGE_SHIM_ID, default, } from "./plugin.js";
|
|
2
|
+
export { resolveOptions, resolveImageConfig, resolveBlurOptions, DEFAULT_OPTIONS, DEFAULT_BLUR_OPTIONS, SUPPORTED_EXTENSIONS, } from "./types.js";
|
|
3
|
+
export { processImage, makeBlurDataURL, toGeneratedPath, toPublicSrc, } from "./process_image.js";
|
|
4
|
+
export { renderManifest, writeManifest, } from "./manifest.js";
|
|
5
|
+
export { isFresh, loadCache, saveCache, } from "./cache.js";
|
|
6
|
+
export { collectImages, run, } from "./run.js";
|
|
7
|
+
export { getImageBlurSvg, } from "./blur_svg.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function renderManifest(entries) {
|
|
4
|
+
const images = {};
|
|
5
|
+
const sorted = [...entries].sort((a, b) => a.originalSrc.localeCompare(b.originalSrc));
|
|
6
|
+
for (const entry of sorted) {
|
|
7
|
+
images[entry.originalSrc] = entry;
|
|
8
|
+
}
|
|
9
|
+
const manifest = { images };
|
|
10
|
+
return JSON.stringify(manifest, null, 2);
|
|
11
|
+
}
|
|
12
|
+
export async function writeManifest(manifestPath, entries) {
|
|
13
|
+
const rendered = renderManifest(entries);
|
|
14
|
+
let current = "";
|
|
15
|
+
try {
|
|
16
|
+
current = await readFile(manifestPath, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// file does not exist yet
|
|
20
|
+
}
|
|
21
|
+
if (current === rendered)
|
|
22
|
+
return;
|
|
23
|
+
await mkdir(path.dirname(manifestPath), { recursive: true });
|
|
24
|
+
await writeFile(manifestPath, rendered, "utf8");
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { getImageProps as nextGetImageProps } from "next/image";
|
|
3
|
+
import type { ImageProps } from "next/image";
|
|
4
|
+
import { getImageBlurSvg } from "./blur_svg.js";
|
|
5
|
+
import type { OptimizedImage } from "./types.js";
|
|
6
|
+
export type ManifestEntry = OptimizedImage;
|
|
7
|
+
export default function Image(props: ImageProps): React.JSX.Element;
|
|
8
|
+
export declare function getImageProps(props: ImageProps): ReturnType<typeof nextGetImageProps>;
|
|
9
|
+
export { getImageBlurSvg };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import NextImage, { getImageProps as nextGetImageProps } from "next/image";
|
|
3
|
+
import manifest from "virtual:cloudflare-next-intl-images-manifest";
|
|
4
|
+
import { getImageBlurSvg } from "./blur_svg.js";
|
|
5
|
+
const manifestData = manifest;
|
|
6
|
+
const images = (manifestData && typeof manifestData === "object" && manifestData.images)
|
|
7
|
+
? manifestData.images
|
|
8
|
+
: {};
|
|
9
|
+
function resolveProps(props) {
|
|
10
|
+
let src = props.src;
|
|
11
|
+
let blurDataURL = props.blurDataURL;
|
|
12
|
+
let width = props.width;
|
|
13
|
+
let height = props.height;
|
|
14
|
+
if (typeof src === "string") {
|
|
15
|
+
const entry = images[src];
|
|
16
|
+
if (entry) {
|
|
17
|
+
if (entry.src)
|
|
18
|
+
src = entry.src;
|
|
19
|
+
if (!blurDataURL && props.placeholder === "blur" && entry.blurDataURL) {
|
|
20
|
+
blurDataURL = getImageBlurSvg(entry.blurDataURL, entry.blurWidth, entry.blurHeight, props.style?.objectFit);
|
|
21
|
+
}
|
|
22
|
+
if (!width && !props.fill && entry.width) {
|
|
23
|
+
width = entry.width;
|
|
24
|
+
height = entry.height;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { ...props, src, blurDataURL, width, height };
|
|
29
|
+
}
|
|
30
|
+
export default function Image(props) {
|
|
31
|
+
const resolved = resolveProps(props);
|
|
32
|
+
return _jsx(NextImage, { ...resolved });
|
|
33
|
+
}
|
|
34
|
+
export function getImageProps(props) {
|
|
35
|
+
const resolved = resolveProps(props);
|
|
36
|
+
return nextGetImageProps(resolved);
|
|
37
|
+
}
|
|
38
|
+
export { getImageBlurSvg };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import type { ImageOptimizerPluginOptions } from "./types.js";
|
|
3
|
+
export declare const VIRTUAL_IMAGE_SHIM_ID = "virtual:cloudflare-next-intl-image";
|
|
4
|
+
export declare const VIRTUAL_MANIFEST_ID = "virtual:cloudflare-next-intl-images-manifest";
|
|
5
|
+
export declare function getShimPath(dir?: string): string;
|
|
6
|
+
export declare function imageOptimizerPlugin(options?: ImageOptimizerPluginOptions): Plugin;
|
|
7
|
+
export declare const imageOptimizer: typeof imageOptimizerPlugin;
|
|
8
|
+
export default imageOptimizerPlugin;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { run } from "./run.js";
|
|
5
|
+
import { resolveOptions } from "./types.js";
|
|
6
|
+
export const VIRTUAL_IMAGE_SHIM_ID = "virtual:cloudflare-next-intl-image";
|
|
7
|
+
export const VIRTUAL_MANIFEST_ID = "virtual:cloudflare-next-intl-images-manifest";
|
|
8
|
+
const RESOLVED_MANIFEST_ID = "\0" + VIRTUAL_MANIFEST_ID;
|
|
9
|
+
export function getShimPath(dir = path.dirname(fileURLToPath(import.meta.url))) {
|
|
10
|
+
const jsPath = path.join(dir, "next_image_shim.js");
|
|
11
|
+
if (existsSync(jsPath))
|
|
12
|
+
return jsPath;
|
|
13
|
+
return path.join(dir, "next_image_shim.tsx");
|
|
14
|
+
}
|
|
15
|
+
export function imageOptimizerPlugin(options) {
|
|
16
|
+
const resolved = resolveOptions(options);
|
|
17
|
+
return {
|
|
18
|
+
name: "cloudflare-next-intl-image-optimizer",
|
|
19
|
+
enforce: "pre",
|
|
20
|
+
apply: resolved.dev ? undefined : "build",
|
|
21
|
+
resolveId(id) {
|
|
22
|
+
if (id === VIRTUAL_IMAGE_SHIM_ID) {
|
|
23
|
+
return getShimPath();
|
|
24
|
+
}
|
|
25
|
+
if (id === VIRTUAL_MANIFEST_ID) {
|
|
26
|
+
return RESOLVED_MANIFEST_ID;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
},
|
|
30
|
+
load(id) {
|
|
31
|
+
if (id === RESOLVED_MANIFEST_ID) {
|
|
32
|
+
const manifestPath = path.resolve(process.cwd(), resolved.manifest);
|
|
33
|
+
if (existsSync(manifestPath)) {
|
|
34
|
+
const content = readFileSync(manifestPath, "utf8");
|
|
35
|
+
return `export default ${content};`;
|
|
36
|
+
}
|
|
37
|
+
return `export default { images: {} };`;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
},
|
|
41
|
+
transform(code, id) {
|
|
42
|
+
if (!resolved.enabled)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (id.includes("node_modules"))
|
|
45
|
+
return undefined;
|
|
46
|
+
if (id === getShimPath())
|
|
47
|
+
return undefined;
|
|
48
|
+
if (!/from\s*["']next\/image["']/.test(code))
|
|
49
|
+
return undefined;
|
|
50
|
+
const next = code.replace(/(import\s+(?!type\s)[^;]*?from\s*)(["'])next\/image\2/g, `$1$2${VIRTUAL_IMAGE_SHIM_ID}$2`);
|
|
51
|
+
return next === code ? undefined : { code: next, map: null };
|
|
52
|
+
},
|
|
53
|
+
async buildStart() {
|
|
54
|
+
if (!resolved.enabled)
|
|
55
|
+
return;
|
|
56
|
+
const root = process.cwd();
|
|
57
|
+
const cacheFile = path.resolve(root, resolved.cacheDir, "manifest.json");
|
|
58
|
+
const entries = await run(root, resolved, cacheFile);
|
|
59
|
+
this.info?.(`[cloudflare-next-intl] image-optimizer: ${entries.length} images in ${resolved.manifest}`);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export const imageOptimizer = imageOptimizerPlugin;
|
|
64
|
+
export default imageOptimizerPlugin;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { OptimizedImage, ResolvedBlurOptions, ResolvedOptions } from "./types.js";
|
|
2
|
+
export declare function toPublicSrc(absolutePath: string, publicRoot: string): string;
|
|
3
|
+
export declare function toGeneratedPath(absolutePath: string, publicRoot: string, outDir: string, root: string): {
|
|
4
|
+
targetFile: string;
|
|
5
|
+
targetSrc: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function makeBlurDataURL(targetFile: string, sourceWidth: number, sourceHeight: number, blurOptions: ResolvedBlurOptions): Promise<{
|
|
8
|
+
blurDataURL: string;
|
|
9
|
+
blurWidth: number;
|
|
10
|
+
blurHeight: number;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function processImage(absolutePath: string, publicRoot: string, options: ResolvedOptions, root?: string): Promise<OptimizedImage>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import sharp from "sharp";
|
|
4
|
+
import { resolveImageConfig } from "./types.js";
|
|
5
|
+
export function toPublicSrc(absolutePath, publicRoot) {
|
|
6
|
+
const relative = path.relative(publicRoot, absolutePath);
|
|
7
|
+
return `/${relative.split(path.sep).join("/")}`;
|
|
8
|
+
}
|
|
9
|
+
export function toGeneratedPath(absolutePath, publicRoot, outDir, root) {
|
|
10
|
+
const relative = path.relative(publicRoot, absolutePath);
|
|
11
|
+
const resolvedOutDir = path.resolve(root, outDir);
|
|
12
|
+
const targetFile = path.join(resolvedOutDir, relative);
|
|
13
|
+
const outDirRelativePublic = path.relative(publicRoot, resolvedOutDir);
|
|
14
|
+
const targetSrc = `/${path.join(outDirRelativePublic, relative).split(path.sep).join("/")}`;
|
|
15
|
+
return { targetFile, targetSrc };
|
|
16
|
+
}
|
|
17
|
+
export async function makeBlurDataURL(targetFile, sourceWidth, sourceHeight, blurOptions) {
|
|
18
|
+
const blurFile = targetFile.replace(/\.[^.]+$/, ".blur.webp");
|
|
19
|
+
let blurWidth;
|
|
20
|
+
let blurHeight;
|
|
21
|
+
if (sourceWidth >= sourceHeight) {
|
|
22
|
+
blurWidth = blurOptions.size;
|
|
23
|
+
blurHeight = Math.max(Math.round((sourceHeight / sourceWidth) * blurOptions.size), 1);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
blurWidth = Math.max(Math.round((sourceWidth / sourceHeight) * blurOptions.size), 1);
|
|
27
|
+
blurHeight = blurOptions.size;
|
|
28
|
+
}
|
|
29
|
+
const buffer = await sharp(targetFile)
|
|
30
|
+
.resize({ width: blurWidth, height: blurHeight, fit: "inside" })
|
|
31
|
+
.webp({ quality: blurOptions.quality })
|
|
32
|
+
.toBuffer();
|
|
33
|
+
await sharp(buffer).toFile(blurFile);
|
|
34
|
+
return {
|
|
35
|
+
blurDataURL: `data:image/webp;base64,${buffer.toString("base64")}`,
|
|
36
|
+
blurWidth,
|
|
37
|
+
blurHeight,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function encodeSibling(targetFile, sourcePath, format, quality, maxWidth, needsResize) {
|
|
41
|
+
const target = targetFile.replace(/\.[^.]+$/, `.${format}`);
|
|
42
|
+
let pipeline = sharp(sourcePath);
|
|
43
|
+
if (needsResize) {
|
|
44
|
+
pipeline = pipeline.resize({ width: maxWidth });
|
|
45
|
+
}
|
|
46
|
+
const encoded = format === "avif"
|
|
47
|
+
? pipeline.avif({ quality })
|
|
48
|
+
: pipeline.webp({ quality });
|
|
49
|
+
await encoded.toFile(target);
|
|
50
|
+
}
|
|
51
|
+
export async function processImage(absolutePath, publicRoot, options, root = path.dirname(publicRoot)) {
|
|
52
|
+
const publicSrc = toPublicSrc(absolutePath, publicRoot);
|
|
53
|
+
const config = resolveImageConfig(publicSrc, options);
|
|
54
|
+
const metadata = await sharp(absolutePath).metadata();
|
|
55
|
+
const sourceWidth = metadata.width;
|
|
56
|
+
const sourceHeight = metadata.height;
|
|
57
|
+
const needsResize = typeof config.maxWidth === "number" && sourceWidth > config.maxWidth;
|
|
58
|
+
const width = needsResize ? config.maxWidth : sourceWidth;
|
|
59
|
+
const height = needsResize
|
|
60
|
+
? Math.round((sourceHeight * config.maxWidth) / sourceWidth)
|
|
61
|
+
: sourceHeight;
|
|
62
|
+
const { targetFile, targetSrc } = toGeneratedPath(absolutePath, publicRoot, options.outDir, root);
|
|
63
|
+
await mkdir(path.dirname(targetFile), { recursive: true });
|
|
64
|
+
let pipeline = sharp(absolutePath);
|
|
65
|
+
if (needsResize) {
|
|
66
|
+
pipeline = pipeline.resize({ width: config.maxWidth });
|
|
67
|
+
}
|
|
68
|
+
const extension = path.extname(absolutePath).toLowerCase();
|
|
69
|
+
const encoded = extension === ".png"
|
|
70
|
+
? pipeline.png({ quality: config.quality, compressionLevel: 9 })
|
|
71
|
+
: pipeline.jpeg({ quality: config.quality, mozjpeg: true });
|
|
72
|
+
await encoded.toFile(targetFile);
|
|
73
|
+
for (const format of config.formats) {
|
|
74
|
+
await encodeSibling(targetFile, absolutePath, format, config.quality, config.maxWidth, needsResize);
|
|
75
|
+
}
|
|
76
|
+
let blurResult;
|
|
77
|
+
if (config.blur.enabled) {
|
|
78
|
+
blurResult = await makeBlurDataURL(targetFile, width, height, config.blur);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
originalSrc: publicSrc,
|
|
82
|
+
src: targetSrc,
|
|
83
|
+
width,
|
|
84
|
+
height,
|
|
85
|
+
blurDataURL: blurResult?.blurDataURL,
|
|
86
|
+
blurWidth: blurResult?.blurWidth,
|
|
87
|
+
blurHeight: blurResult?.blurHeight,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isFresh, loadCache, saveCache } from "./cache.js";
|
|
4
|
+
import { writeManifest } from "./manifest.js";
|
|
5
|
+
import { processImage, toGeneratedPath, toPublicSrc } from "./process_image.js";
|
|
6
|
+
import { resolveImageConfig, SUPPORTED_EXTENSIONS } from "./types.js";
|
|
7
|
+
async function walk(directory, found) {
|
|
8
|
+
let items;
|
|
9
|
+
try {
|
|
10
|
+
items = await readdir(directory, { withFileTypes: true });
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
for (const item of items) {
|
|
16
|
+
const full = path.join(directory, item.name);
|
|
17
|
+
if (item.isDirectory()) {
|
|
18
|
+
await walk(full, found);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (SUPPORTED_EXTENSIONS.includes(path.extname(item.name).toLowerCase())) {
|
|
22
|
+
found.push(full);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function collectImages(dirs, root) {
|
|
27
|
+
const found = [];
|
|
28
|
+
for (const dir of dirs) {
|
|
29
|
+
await walk(path.resolve(root, dir), found);
|
|
30
|
+
}
|
|
31
|
+
return found.sort();
|
|
32
|
+
}
|
|
33
|
+
function targetAndSiblingPaths(absolutePath, publicRoot, options, root) {
|
|
34
|
+
const publicSrc = toPublicSrc(absolutePath, publicRoot);
|
|
35
|
+
const config = resolveImageConfig(publicSrc, options);
|
|
36
|
+
const { targetFile } = toGeneratedPath(absolutePath, publicRoot, options.outDir, root);
|
|
37
|
+
const siblings = config.formats.map((format) => targetFile.replace(/\.[^.]+$/, `.${format}`));
|
|
38
|
+
const result = [targetFile, ...siblings];
|
|
39
|
+
if (config.blur.enabled) {
|
|
40
|
+
result.push(targetFile.replace(/\.[^.]+$/, ".blur.webp"));
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
export async function run(root, options, cacheFile = path.resolve(root, options.cacheDir, "manifest.json")) {
|
|
45
|
+
const publicRoot = path.resolve(root, "public");
|
|
46
|
+
const manifestPath = path.resolve(root, options.manifest);
|
|
47
|
+
const cache = await loadCache(cacheFile);
|
|
48
|
+
const nextCache = {};
|
|
49
|
+
const files = await collectImages(options.dirs, root);
|
|
50
|
+
const entries = [];
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const relativeKey = path.relative(root, file);
|
|
53
|
+
const cached = cache[relativeKey];
|
|
54
|
+
const targets = targetAndSiblingPaths(file, publicRoot, options, root);
|
|
55
|
+
const fresh = await isFresh(file, cached, targets);
|
|
56
|
+
if (fresh && cached) {
|
|
57
|
+
entries.push(cached.result);
|
|
58
|
+
nextCache[relativeKey] = cached;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const result = await processImage(file, publicRoot, options, root);
|
|
62
|
+
const fileStat = await stat(file);
|
|
63
|
+
entries.push(result);
|
|
64
|
+
nextCache[relativeKey] = {
|
|
65
|
+
mtimeMs: fileStat.mtimeMs,
|
|
66
|
+
size: fileStat.size,
|
|
67
|
+
result,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
await saveCache(cacheFile, nextCache);
|
|
71
|
+
await writeManifest(manifestPath, entries);
|
|
72
|
+
return entries;
|
|
73
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import sharp from "sharp";
|
|
5
|
+
export async function makeTempDir() {
|
|
6
|
+
return mkdtemp(path.join(tmpdir(), "cfni-img-test-"));
|
|
7
|
+
}
|
|
8
|
+
export async function cleanup(dir) {
|
|
9
|
+
await rm(dir, { recursive: true, force: true });
|
|
10
|
+
}
|
|
11
|
+
export async function writeFixturePng(dir, filename, width, height) {
|
|
12
|
+
await mkdir(dir, { recursive: true });
|
|
13
|
+
const target = path.join(dir, filename);
|
|
14
|
+
await sharp({
|
|
15
|
+
create: {
|
|
16
|
+
width,
|
|
17
|
+
height,
|
|
18
|
+
channels: 4,
|
|
19
|
+
background: { r: 120, g: 180, b: 240, alpha: 1 },
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
.png()
|
|
23
|
+
.toFile(target);
|
|
24
|
+
return target;
|
|
25
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export type ImageFormat = "avif" | "webp";
|
|
2
|
+
export interface ImageBlurOptions {
|
|
3
|
+
/** Enable blur placeholder generation. Default: true */
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
/** Tiny thumbnail dimension (largest side). Default: 8 */
|
|
6
|
+
size?: number;
|
|
7
|
+
/** WebP thumbnail quality. Default: 70 */
|
|
8
|
+
quality?: number;
|
|
9
|
+
/** Gaussian blur stdDeviation for SVG filter. Default: 20 */
|
|
10
|
+
stdDeviation?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ImageOverrideOptions {
|
|
13
|
+
/** Formats to emit for this image, or `false` to disable format conversions. Default: inherits global */
|
|
14
|
+
formats?: ImageFormat[] | false;
|
|
15
|
+
/** Max width to downscale, or `false` to preserve original dimensions. Default: inherits global */
|
|
16
|
+
maxWidth?: number | false;
|
|
17
|
+
/** Compression quality (1-100). Default: inherits global */
|
|
18
|
+
quality?: number;
|
|
19
|
+
/** Blur placeholder settings for this image, or `false` to disable. Default: inherits global */
|
|
20
|
+
blur?: boolean | ImageBlurOptions;
|
|
21
|
+
}
|
|
22
|
+
export interface ImageOptimizerPluginOptions {
|
|
23
|
+
/** Enable or disable image optimization. Default: true */
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
/** Directories scanned recursively relative to project root. Default: ["public/images", "public/icons"] */
|
|
26
|
+
dirs?: string[];
|
|
27
|
+
/** Target directory for optimized assets. Default: "public/generated" */
|
|
28
|
+
outDir?: string;
|
|
29
|
+
/** Max width to downscale oversized images, or `false` to disable. Default: 1920 */
|
|
30
|
+
maxWidth?: number | false;
|
|
31
|
+
/** Compression quality for rasters. Default: 80 */
|
|
32
|
+
quality?: number;
|
|
33
|
+
/** Target sibling formats, or `false` to disable format conversions. Default: ["avif", "webp"] */
|
|
34
|
+
formats?: ImageFormat[] | false;
|
|
35
|
+
/** Output path for generated JSON manifest. Default: "public/generated/images.json" */
|
|
36
|
+
manifest?: string;
|
|
37
|
+
/** Global blur placeholder settings, or `false` to disable blur generation. Default: true */
|
|
38
|
+
blur?: boolean | ImageBlurOptions;
|
|
39
|
+
/** Run on dev server as well as production build. Default: true */
|
|
40
|
+
dev?: boolean;
|
|
41
|
+
/** Cache directory. Default: "node_modules/.cache/cloudflare-next-intl/image-optimizer" */
|
|
42
|
+
cacheDir?: string;
|
|
43
|
+
/** Per-image overrides keyed by public src (e.g. `"/images/hero.png"`) */
|
|
44
|
+
overrides?: Record<string, ImageOverrideOptions>;
|
|
45
|
+
}
|
|
46
|
+
export interface ResolvedBlurOptions {
|
|
47
|
+
enabled: boolean;
|
|
48
|
+
size: number;
|
|
49
|
+
quality: number;
|
|
50
|
+
stdDeviation: number;
|
|
51
|
+
}
|
|
52
|
+
export interface ResolvedOptions {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
dirs: string[];
|
|
55
|
+
outDir: string;
|
|
56
|
+
maxWidth: number | false;
|
|
57
|
+
quality: number;
|
|
58
|
+
formats: ImageFormat[];
|
|
59
|
+
manifest: string;
|
|
60
|
+
blur: ResolvedBlurOptions;
|
|
61
|
+
dev: boolean;
|
|
62
|
+
cacheDir: string;
|
|
63
|
+
overrides: Record<string, ImageOverrideOptions>;
|
|
64
|
+
}
|
|
65
|
+
export interface ResolvedImageConfig {
|
|
66
|
+
maxWidth: number | false;
|
|
67
|
+
quality: number;
|
|
68
|
+
formats: ImageFormat[];
|
|
69
|
+
blur: ResolvedBlurOptions;
|
|
70
|
+
}
|
|
71
|
+
export interface OptimizedImage {
|
|
72
|
+
originalSrc: string;
|
|
73
|
+
src: string;
|
|
74
|
+
width: number;
|
|
75
|
+
height: number;
|
|
76
|
+
blurDataURL?: string;
|
|
77
|
+
blurWidth?: number;
|
|
78
|
+
blurHeight?: number;
|
|
79
|
+
}
|
|
80
|
+
export interface ManifestData {
|
|
81
|
+
images: Record<string, OptimizedImage>;
|
|
82
|
+
}
|
|
83
|
+
export declare const SUPPORTED_EXTENSIONS: readonly string[];
|
|
84
|
+
export declare const DEFAULT_BLUR_OPTIONS: ResolvedBlurOptions;
|
|
85
|
+
export declare const DEFAULT_OPTIONS: ResolvedOptions;
|
|
86
|
+
export declare function resolveBlurOptions(blur: boolean | ImageBlurOptions | undefined, parentDefault?: ResolvedBlurOptions): ResolvedBlurOptions;
|
|
87
|
+
export declare function resolveOptions(options: ImageOptimizerPluginOptions | undefined): ResolvedOptions;
|
|
88
|
+
export declare function resolveImageConfig(publicSrc: string, options: ResolvedOptions): ResolvedImageConfig;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const SUPPORTED_EXTENSIONS = [".png", ".jpg", ".jpeg"];
|
|
2
|
+
export const DEFAULT_BLUR_OPTIONS = {
|
|
3
|
+
enabled: true,
|
|
4
|
+
size: 8,
|
|
5
|
+
quality: 70,
|
|
6
|
+
stdDeviation: 20,
|
|
7
|
+
};
|
|
8
|
+
export const DEFAULT_OPTIONS = {
|
|
9
|
+
enabled: true,
|
|
10
|
+
dirs: ["public/images", "public/icons"],
|
|
11
|
+
outDir: "public/generated",
|
|
12
|
+
maxWidth: 1920,
|
|
13
|
+
quality: 80,
|
|
14
|
+
formats: ["avif", "webp"],
|
|
15
|
+
manifest: "public/generated/images.json",
|
|
16
|
+
blur: DEFAULT_BLUR_OPTIONS,
|
|
17
|
+
dev: true,
|
|
18
|
+
cacheDir: "node_modules/.cache/cloudflare-next-intl/image-optimizer",
|
|
19
|
+
overrides: {},
|
|
20
|
+
};
|
|
21
|
+
export function resolveBlurOptions(blur, parentDefault = DEFAULT_BLUR_OPTIONS) {
|
|
22
|
+
if (blur === false) {
|
|
23
|
+
return { ...parentDefault, enabled: false };
|
|
24
|
+
}
|
|
25
|
+
if (blur === true) {
|
|
26
|
+
return { ...parentDefault, enabled: true };
|
|
27
|
+
}
|
|
28
|
+
if (blur === undefined) {
|
|
29
|
+
return { ...parentDefault };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
enabled: blur.enabled ?? parentDefault.enabled,
|
|
33
|
+
size: blur.size ?? parentDefault.size,
|
|
34
|
+
quality: blur.quality ?? parentDefault.quality,
|
|
35
|
+
stdDeviation: blur.stdDeviation ?? parentDefault.stdDeviation,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function resolveOptions(options) {
|
|
39
|
+
const raw = options ?? {};
|
|
40
|
+
const formats = raw.formats === false ? [] : (raw.formats ?? DEFAULT_OPTIONS.formats);
|
|
41
|
+
const maxWidth = raw.maxWidth === false ? false : (raw.maxWidth ?? DEFAULT_OPTIONS.maxWidth);
|
|
42
|
+
const blur = resolveBlurOptions(raw.blur, DEFAULT_BLUR_OPTIONS);
|
|
43
|
+
return {
|
|
44
|
+
enabled: raw.enabled ?? DEFAULT_OPTIONS.enabled,
|
|
45
|
+
dirs: raw.dirs ? [...raw.dirs] : [...DEFAULT_OPTIONS.dirs],
|
|
46
|
+
outDir: raw.outDir ?? DEFAULT_OPTIONS.outDir,
|
|
47
|
+
maxWidth,
|
|
48
|
+
quality: raw.quality ?? DEFAULT_OPTIONS.quality,
|
|
49
|
+
formats: [...formats],
|
|
50
|
+
manifest: raw.manifest ?? DEFAULT_OPTIONS.manifest,
|
|
51
|
+
blur,
|
|
52
|
+
dev: raw.dev ?? DEFAULT_OPTIONS.dev,
|
|
53
|
+
cacheDir: raw.cacheDir ?? DEFAULT_OPTIONS.cacheDir,
|
|
54
|
+
overrides: raw.overrides ? { ...raw.overrides } : {},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function resolveImageConfig(publicSrc, options) {
|
|
58
|
+
const override = options.overrides[publicSrc];
|
|
59
|
+
if (!override) {
|
|
60
|
+
return {
|
|
61
|
+
maxWidth: options.maxWidth,
|
|
62
|
+
quality: options.quality,
|
|
63
|
+
formats: options.formats,
|
|
64
|
+
blur: options.blur,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const formats = override.formats === false
|
|
68
|
+
? []
|
|
69
|
+
: override.formats !== undefined
|
|
70
|
+
? [...override.formats]
|
|
71
|
+
: options.formats;
|
|
72
|
+
const maxWidth = override.maxWidth === false
|
|
73
|
+
? false
|
|
74
|
+
: override.maxWidth !== undefined
|
|
75
|
+
? override.maxWidth
|
|
76
|
+
: options.maxWidth;
|
|
77
|
+
const quality = override.quality ?? options.quality;
|
|
78
|
+
const blur = override.blur !== undefined
|
|
79
|
+
? resolveBlurOptions(override.blur, options.blur)
|
|
80
|
+
: options.blur;
|
|
81
|
+
return { maxWidth, quality, formats, blur };
|
|
82
|
+
}
|
package/dist/src/vite/index.d.ts
CHANGED
|
@@ -3,3 +3,4 @@ export { userAgentStubPlugin, USER_AGENT_STUB_ID, USER_AGENT_STUB_CODE } from ".
|
|
|
3
3
|
export { cfWorkersClientStubPlugin, CF_WORKERS_CLIENT_STUB_ID, CF_WORKERS_CLIENT_STUB_CODE } from "./cf_workers_client_stub.js";
|
|
4
4
|
export { localeFilePlugin, resolveDefaultIntlConfigPath, type LocaleFilePluginOptions } from "./locale_file_plugin.js";
|
|
5
5
|
export { cloudflareNextIntl, cloudflareNextIntlPlugin, type CloudflareNextIntlOptions, default } from "./plugin.js";
|
|
6
|
+
export { imageOptimizer, imageOptimizerPlugin, VIRTUAL_IMAGE_SHIM_ID, type ImageFormat, type ImageBlurOptions, type ImageOverrideOptions, type ImageOptimizerPluginOptions, type ResolvedBlurOptions, type ResolvedOptions, type ResolvedImageConfig, type OptimizedImage, type ManifestData, type ManifestEntry, } from "../image_optimizer/index.js";
|
package/dist/src/vite/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { userAgentStubPlugin, USER_AGENT_STUB_ID, USER_AGENT_STUB_CODE } from ".
|
|
|
3
3
|
export { cfWorkersClientStubPlugin, CF_WORKERS_CLIENT_STUB_ID, CF_WORKERS_CLIENT_STUB_CODE } from "./cf_workers_client_stub.js";
|
|
4
4
|
export { localeFilePlugin, resolveDefaultIntlConfigPath } from "./locale_file_plugin.js";
|
|
5
5
|
export { cloudflareNextIntl, cloudflareNextIntlPlugin, default } from "./plugin.js";
|
|
6
|
+
export { imageOptimizer, imageOptimizerPlugin, VIRTUAL_IMAGE_SHIM_ID, } from "../image_optimizer/index.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
2
|
import { type LocaleFilePluginOptions } from "./locale_file_plugin.js";
|
|
3
|
+
import { type ImageOptimizerPluginOptions } from "../image_optimizer/index.js";
|
|
3
4
|
export interface CloudflareNextIntlOptions extends LocaleFilePluginOptions {
|
|
4
5
|
/**
|
|
5
6
|
* Emit static `BUILD_ID` asset on client build for Vinext / Cloudflare.
|
|
@@ -23,6 +24,13 @@ export interface CloudflareNextIntlOptions extends LocaleFilePluginOptions {
|
|
|
23
24
|
* @default true
|
|
24
25
|
*/
|
|
25
26
|
cfWorkersClientStub?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Build-time and dev image optimizer plugin. Automatically downscales rasters into `public/generated`,
|
|
29
|
+
* emits AVIF / WebP siblings, generates blur placeholders with SVG filters, and injects blurDataURL.
|
|
30
|
+
* Pass an options object to customize or `false` to disable.
|
|
31
|
+
* @default true
|
|
32
|
+
*/
|
|
33
|
+
imageOptimizer?: boolean | ImageOptimizerPluginOptions;
|
|
26
34
|
}
|
|
27
35
|
export declare function cloudflareNextIntl(options?: CloudflareNextIntlOptions): Plugin[];
|
|
28
36
|
export declare const cloudflareNextIntlPlugin: typeof cloudflareNextIntl;
|
package/dist/src/vite/plugin.js
CHANGED
|
@@ -2,8 +2,14 @@ import { buildIdAsset } from "./build_id_asset.js";
|
|
|
2
2
|
import { userAgentStubPlugin } from "./user_agent_stub.js";
|
|
3
3
|
import { cfWorkersClientStubPlugin } from "./cf_workers_client_stub.js";
|
|
4
4
|
import { localeFilePlugin } from "./locale_file_plugin.js";
|
|
5
|
+
import { imageOptimizerPlugin } from "../image_optimizer/index.js";
|
|
5
6
|
export function cloudflareNextIntl(options = {}) {
|
|
6
7
|
const plugins = [];
|
|
8
|
+
if (options.imageOptimizer !== false) {
|
|
9
|
+
plugins.push(imageOptimizerPlugin(typeof options.imageOptimizer === "object"
|
|
10
|
+
? options.imageOptimizer
|
|
11
|
+
: undefined));
|
|
12
|
+
}
|
|
7
13
|
if (options.buildIdAsset !== false) {
|
|
8
14
|
const fileName = typeof options.buildIdAsset === "string" ? options.buildIdAsset : "BUILD_ID";
|
|
9
15
|
plugins.push(buildIdAsset(fileName));
|
package/llms.txt
CHANGED
|
@@ -27,7 +27,8 @@ other subpath can be used.
|
|
|
27
27
|
- `./db` — `withPublicDb(fn)` / `withUserDb(fn, uid?)` server-side Postgres/Drizzle context helpers (require `db` set on your `RoutingConfig`; direct Postgres or Supabase Data API with automatic PostgREST REST translation and `cfni_exec` fallback, see below).
|
|
28
28
|
- `./dbEslint` — flat-config ESLint fragment banning direct `@supabase/supabase-js`, `pg`, `postgres`, and deep `dist/` imports in application code.
|
|
29
29
|
- `./dbHelpers` — generic Drizzle SQL helper functions (`excluded`, `onConflictSet`, `ago`, `currentDate`, `windowCount`, `unnestLateral`, `ascNullsLast`, `alwaysTrue`, `lateral`, `aliasColumn`, `minOf`, `maxOf`, `roundReal`, `multiply`, `scalarFromCte`) for use with `./db`.
|
|
30
|
-
- `./vite` — `cloudflareNextIntl(options?)` / `cloudflareNextIntlPlugin`, `buildIdAsset(fileName?)`, `localeFilePlugin(options?)`, `userAgentStubPlugin()`, `cfWorkersClientStubPlugin()`: All-in-one Vite plugin required for Vinext/Cloudflare Workers environments (bundles `@locale-file/*` via eager glob, resolves `@intl-config`, stubs Node.js `user-agent` to prevent runtime `node:fs` errors, stubs `cloudflare:workers` for client builds,
|
|
30
|
+
- `./vite` — `cloudflareNextIntl(options?)` / `cloudflareNextIntlPlugin`, `imageOptimizerPlugin(options?)` / `imageOptimizer`, `buildIdAsset(fileName?)`, `localeFilePlugin(options?)`, `userAgentStubPlugin()`, `cfWorkersClientStubPlugin()`: All-in-one Vite plugin required for Vinext/Cloudflare Workers environments (bundles `@locale-file/*` via eager glob, resolves `@intl-config`, stubs Node.js `user-agent` to prevent runtime `node:fs` errors, stubs `cloudflare:workers` for client builds, emits client `BUILD_ID`, and runs build-time/dev Image Optimizer with Next.js blur placeholder shimming).
|
|
31
|
+
- `./image-optimizer` / `./imageOptimizer` — image optimization suite: `imageOptimizerPlugin`, `imageOptimizer`, `resolveOptions`, `resolveImageConfig`, `resolveBlurOptions`, `processImage`, `makeBlurDataURL`, `getImageBlurSvg`, `renderManifest`, `writeManifest`, `isFresh`, `loadCache`, `saveCache`, `collectImages`, `run`.
|
|
31
32
|
- `./errorHandling` — error reporting & stale deploy recovery barrel: `reportError`, `withErrorHandling`, `installConsoleErrorOverride`, `installGlobalErrorOverride`, `stringifyUnknown`, `formatErrorMessage`, `defaultIgnoredConsoleErrors`, `createServerErrorAction`, `isStaleDeployError`, `defaultStaleDeployPatterns`, `setStaleDeployPatterns`, `getStaleDeployPatterns`, `clearClientCache`, `useStaleDeployRecovery`, `shouldRecoverFromStaleDeploy`, `isRecentBuild`.
|
|
32
33
|
- `./isStaleDeployError` — `isStaleDeployError(error, patterns?)`, `setStaleDeployPatterns(patterns)`, `getStaleDeployPatterns()`: detector returning `true` for version skew / chunk load / dynamic import / hydration errors (ChunkLoadError, failed to fetch, dynamically imported module failure, loading CSS chunk, connection closed, RSC payload failure, minified error #412, or missing stream error `undefined`) with fast pre-lowercased pattern cache and intl-config integration (`errorHandling.staleDeployPatterns`).
|
|
33
34
|
- `./clearClientCache` — `clearClientCache()`: async helper wiping `window.caches`, unregistering service workers, and clearing `sessionStorage` for recovering from stale deployments.
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudflare-next-intl",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.55",
|
|
4
4
|
"description": "Optimized Next Intl Package Special for App Router and Cloudflare",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"sideEffects": false,
|
|
8
9
|
"bin": {
|
|
9
10
|
"cfni-db-codegen": "bin/db_codegen.mjs",
|
|
10
|
-
"cfni-db-install-exec": "bin/db_install_exec.mjs"
|
|
11
|
+
"cfni-db-install-exec": "bin/db_install_exec.mjs",
|
|
12
|
+
"cfni-image-optimizer": "bin/image_optimizer.mjs",
|
|
13
|
+
"optimize-images": "bin/image_optimizer.mjs"
|
|
11
14
|
},
|
|
12
15
|
"files": [
|
|
13
16
|
"dist",
|
|
@@ -221,6 +224,14 @@
|
|
|
221
224
|
"./vite": {
|
|
222
225
|
"types": "./dist/src/vite/index.d.ts",
|
|
223
226
|
"import": "./dist/src/vite/index.js"
|
|
227
|
+
},
|
|
228
|
+
"./image-optimizer": {
|
|
229
|
+
"types": "./dist/src/image_optimizer/index.d.ts",
|
|
230
|
+
"import": "./dist/src/image_optimizer/index.js"
|
|
231
|
+
},
|
|
232
|
+
"./imageOptimizer": {
|
|
233
|
+
"types": "./dist/src/image_optimizer/index.d.ts",
|
|
234
|
+
"import": "./dist/src/image_optimizer/index.js"
|
|
224
235
|
}
|
|
225
236
|
},
|
|
226
237
|
"scripts": {
|
|
@@ -271,7 +282,8 @@
|
|
|
271
282
|
"embedded-postgres": "^18.4.0-beta.17",
|
|
272
283
|
"firebase": "^12.17.0",
|
|
273
284
|
"jose": "^6.2.8",
|
|
274
|
-
"pg": "^8.23.0"
|
|
285
|
+
"pg": "^8.23.0",
|
|
286
|
+
"sharp": "^0.34.5"
|
|
275
287
|
},
|
|
276
288
|
"peerDependencies": {
|
|
277
289
|
"next": ">=12.0.0",
|