opennextjs-azure 0.1.1 → 0.1.3

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.
@@ -0,0 +1,115 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { BlobServiceClient } from '@azure/storage-blob';
3
+ import { ReadableStream } from 'node:stream/web';
4
+
5
+ const { AZURE_STORAGE_CONNECTION_STRING, AZURE_STORAGE_ACCOUNT_NAME } = process.env;
6
+ const CACHE_CONTAINER = "optimized-images";
7
+ function getBlobClient(key) {
8
+ if (!AZURE_STORAGE_CONNECTION_STRING && !AZURE_STORAGE_ACCOUNT_NAME) {
9
+ throw new Error("Azure Storage connection string or account name must be defined");
10
+ }
11
+ const blobServiceClient = AZURE_STORAGE_CONNECTION_STRING ? BlobServiceClient.fromConnectionString(AZURE_STORAGE_CONNECTION_STRING) : new BlobServiceClient(`https://${AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net`);
12
+ const containerClient = blobServiceClient.getContainerClient(CACHE_CONTAINER);
13
+ return containerClient.getBlockBlobClient(key);
14
+ }
15
+ function computeCacheKey(event) {
16
+ const { query } = event;
17
+ const url = Array.isArray(query?.url) ? query.url[0] : query?.url || "";
18
+ const width = Array.isArray(query?.w) ? query.w[0] : query?.w || "0";
19
+ const quality = Array.isArray(query?.q) ? query.q[0] : query?.q || "75";
20
+ const hash = createHash("sha256").update(url).digest("hex").substring(0, 16);
21
+ return `${hash}/w${width}_q${quality}.cache`;
22
+ }
23
+ async function getCachedImage(cacheKey) {
24
+ try {
25
+ const blobClient = getBlobClient(cacheKey);
26
+ const exists = await blobClient.exists();
27
+ if (!exists) {
28
+ return null;
29
+ }
30
+ const downloadResponse = await blobClient.download();
31
+ const properties = await blobClient.getProperties();
32
+ if (!downloadResponse.readableStreamBody) {
33
+ return null;
34
+ }
35
+ const chunks = [];
36
+ for await (const chunk of downloadResponse.readableStreamBody) {
37
+ chunks.push(Buffer.from(chunk));
38
+ }
39
+ const buffer = Buffer.concat(chunks);
40
+ return {
41
+ type: "core",
42
+ statusCode: 200,
43
+ headers: {
44
+ "Content-Type": properties.contentType || "image/webp",
45
+ "Cache-Control": properties.cacheControl || "public,max-age=31536000,immutable",
46
+ Vary: "Accept"
47
+ },
48
+ body: new ReadableStream({
49
+ start(controller) {
50
+ controller.enqueue(buffer);
51
+ controller.close();
52
+ }
53
+ }),
54
+ isBase64Encoded: true
55
+ };
56
+ } catch (error) {
57
+ return null;
58
+ }
59
+ }
60
+ async function setCachedImage(cacheKey, result) {
61
+ try {
62
+ if (!result.body) {
63
+ return;
64
+ }
65
+ const chunks = [];
66
+ for await (const chunk of result.body) {
67
+ chunks.push(Buffer.from(chunk));
68
+ }
69
+ const buffer = Buffer.concat(chunks);
70
+ const blobClient = getBlobClient(cacheKey);
71
+ const contentTypeRaw = result.headers?.["Content-Type"] || result.headers?.["content-type"];
72
+ const contentType = Array.isArray(contentTypeRaw) ? contentTypeRaw[0] : contentTypeRaw || "image/webp";
73
+ const cacheControlRaw = result.headers?.["Cache-Control"] || result.headers?.["cache-control"];
74
+ const cacheControl = Array.isArray(cacheControlRaw) ? cacheControlRaw[0] : cacheControlRaw || "public,max-age=31536000,immutable";
75
+ await blobClient.upload(buffer, buffer.length, {
76
+ blobHTTPHeaders: {
77
+ blobContentType: contentType,
78
+ blobCacheControl: cacheControl
79
+ }
80
+ });
81
+ } catch (error) {
82
+ console.error("Failed to cache optimized image:", error);
83
+ }
84
+ }
85
+ function createCachedImageOptimizationHandler(defaultHandler) {
86
+ return async (event, options) => {
87
+ const cacheKey = computeCacheKey(event);
88
+ const cached = await getCachedImage(cacheKey);
89
+ if (cached) {
90
+ return cached;
91
+ }
92
+ const result = await defaultHandler(event, options);
93
+ if (result.statusCode === 200 && result.body) {
94
+ const chunks = [];
95
+ for await (const chunk of result.body) {
96
+ chunks.push(Buffer.from(chunk));
97
+ }
98
+ const buffer = Buffer.concat(chunks);
99
+ const resultWithBuffer = {
100
+ ...result,
101
+ body: new ReadableStream({
102
+ start(controller) {
103
+ controller.enqueue(buffer);
104
+ controller.close();
105
+ }
106
+ })
107
+ };
108
+ await setCachedImage(cacheKey, resultWithBuffer);
109
+ return resultWithBuffer;
110
+ }
111
+ return result;
112
+ };
113
+ }
114
+
115
+ export { createCachedImageOptimizationHandler };
@@ -1,5 +1,5 @@
1
1
  import { RoutePreloadingBehavior, OpenNextConfig } from '@opennextjs/aws/types/open-next.js';
2
- import { IncrementalCache, TagCache, Queue } from '@opennextjs/aws/types/overrides.js';
2
+ import { IncrementalCache, TagCache, Queue, ImageLoader } from '@opennextjs/aws/types/overrides.js';
3
3
 
4
4
  type AzureDeploymentTarget = "functions" | "static-web-apps" | "container-apps";
5
5
  interface AzureDeploymentConfig {
@@ -19,6 +19,8 @@ interface AzureConfig {
19
19
  incrementalCache?: "azure-blob" | IncrementalCache;
20
20
  tagCache?: "azure-table" | TagCache;
21
21
  queue?: "azure-queue" | Queue;
22
+ imageLoader?: "azure-blob" | ImageLoader | (() => Promise<ImageLoader>);
23
+ enableImageOptimizationCache?: boolean;
22
24
  routePreloadingBehavior?: RoutePreloadingBehavior;
23
25
  middleware?: OpenNextConfig["middleware"];
24
26
  dangerous?: OpenNextConfig["dangerous"];
@@ -1,5 +1,5 @@
1
1
  import { RoutePreloadingBehavior, OpenNextConfig } from '@opennextjs/aws/types/open-next.js';
2
- import { IncrementalCache, TagCache, Queue } from '@opennextjs/aws/types/overrides.js';
2
+ import { IncrementalCache, TagCache, Queue, ImageLoader } from '@opennextjs/aws/types/overrides.js';
3
3
 
4
4
  type AzureDeploymentTarget = "functions" | "static-web-apps" | "container-apps";
5
5
  interface AzureDeploymentConfig {
@@ -19,6 +19,8 @@ interface AzureConfig {
19
19
  incrementalCache?: "azure-blob" | IncrementalCache;
20
20
  tagCache?: "azure-table" | TagCache;
21
21
  queue?: "azure-queue" | Queue;
22
+ imageLoader?: "azure-blob" | ImageLoader | (() => Promise<ImageLoader>);
23
+ enableImageOptimizationCache?: boolean;
22
24
  routePreloadingBehavior?: RoutePreloadingBehavior;
23
25
  middleware?: OpenNextConfig["middleware"];
24
26
  dangerous?: OpenNextConfig["dangerous"];
@@ -96,6 +96,14 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
96
96
  publicAccess: 'Blob'
97
97
  }
98
98
  }
99
+
100
+ // Container for optimized images (public CDN access)
101
+ resource optimizedImagesContainer 'containers' = {
102
+ name: 'optimized-images'
103
+ properties: {
104
+ publicAccess: 'Blob'
105
+ }
106
+ }
99
107
  }
100
108
 
101
109
  // Table service for tag cache
@@ -194,6 +202,10 @@ resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
194
202
  name: 'AZURE_QUEUE_NAME'
195
203
  value: queueName
196
204
  }
205
+ {
206
+ name: 'AZURE_IMAGE_OPTIMIZATION_CACHE'
207
+ value: 'true'
208
+ }
197
209
  {
198
210
  name: 'NODE_ENV'
199
211
  value: 'production'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opennextjs-azure",
3
- "version": "0.1.1",
4
- "description": "Azure adapter for Next.js applications using OpenNext",
3
+ "version": "0.1.3",
4
+ "description": "True serverless Next.js on Azure Functions with a Vercel-grade developer experience",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -69,7 +69,6 @@
69
69
  "bugs": {
70
70
  "url": "https://github.com/zpg6/opennextjs-azure/issues"
71
71
  },
72
- "homepage": "https://opennext.js.org/azure",
73
72
  "dependencies": {
74
73
  "@opennextjs/aws": "^3.8.5",
75
74
  "@azure/functions": "^4.5.1",