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.
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # OpenNext.js Azure
2
2
 
3
- **True serverless Next.js on Azure Functions**
3
+ **True serverless Next.js on Azure Functions** (EXPERIMENTAL)
4
4
 
5
5
  [![NPM Version](https://img.shields.io/npm/v/opennextjs-azure)](https://www.npmjs.com/package/opennextjs-azure)
6
6
  [![NPM Downloads](https://img.shields.io/npm/dt/opennextjs-azure)](https://www.npmjs.com/package/opennextjs-azure)
7
7
  [![License: MIT](https://img.shields.io/npm/l/opennextjs-azure)](https://opensource.org/licenses/MIT)
8
8
 
9
- Built on the [OpenNext](https://opennext.js.org) framework, this adapter brings native Next.js support to Azure Functions.
9
+ Built on the [OpenNext](https://opennext.js.org) framework, this adapter brings native Next.js support to Azure Functions with a Vercel-grade developer experience.
10
+
11
+ ![Deploy Screenshot](./docs/deploy-screenshot.png)
10
12
 
11
13
  > **🚀 New to Azure deployment?** Jump to [Quick Start](#quick-start) and run `npx opennextjs-azure@latest init --scaffold` to create a fully configured Next.js app on Azure in seconds!
12
14
 
@@ -21,6 +23,7 @@ Azure Functions is Microsoft's serverless compute platform—comparable to AWS L
21
23
  | Next.js Feature | Azure Implementation |
22
24
  | -------------------------------------- | --------------------------------------------------------------- |
23
25
  | Incremental Static Regeneration | Azure Blob Storage |
26
+ | Image Optimization | Azure Blob Storage with automatic caching |
24
27
  | Streaming SSR | Azure Functions with Node.js streams |
25
28
  | `revalidateTag()` / `revalidatePath()` | Azure Table Storage + Queue Storage |
26
29
  | Fetch caching | Azure Blob Storage with build ID namespacing |
@@ -118,6 +121,8 @@ Response Stream → Azure Functions Response
118
121
  - Environment variables
119
122
  - Connection strings
120
123
 
124
+ ![Resource Group Screenshot](./docs/rg-screenshot.png)
125
+
121
126
  Choose your environment:
122
127
 
123
128
  - `--environment dev` → Y1 Consumption (pay-per-execution)
@@ -135,6 +140,12 @@ Converts between Azure Functions HTTP triggers and Next.js InternalEvent/Interna
135
140
  - **Tag Cache:** Azure Table Storage maps tags → paths for `revalidateTag()`
136
141
  - **Revalidation Queue:** Azure Queue Storage triggers on-demand regeneration
137
142
 
143
+ **Image Optimization:**
144
+
145
+ - **Source Images:** Loaded from `assets` blob container
146
+ - **Optimized Cache:** Processed images cached in `optimized-images` container to avoid re-processing
147
+ - **Processing:** Processing is done with the `sharp` library, which gets added for you during deployment.
148
+
138
149
  **Build Process:**
139
150
  Uses OpenNext's AWS build with Azure-specific overrides, then adds Azure Functions metadata (`host.json`, `function.json`) for v3 programming model.
140
151
 
@@ -183,6 +194,17 @@ opennextjs-azure deploy \
183
194
  opennextjs-azure tail \
184
195
  [--app-name <name>] \
185
196
  [--resource-group <name>]
197
+
198
+ # Check deployment health
199
+ opennextjs-azure health \
200
+ [--app-name <name>] \
201
+ [--resource-group <name>]
202
+
203
+ # Delete resource group and all resources
204
+ opennextjs-azure delete \
205
+ [--resource-group <name>] \
206
+ [--yes] \
207
+ [--no-wait]
186
208
  ```
187
209
 
188
210
  ## License
@@ -0,0 +1,20 @@
1
+ import { InternalEvent, InternalResult } from '@opennextjs/aws/types/open-next.js';
2
+ import { OpenNextHandler, OpenNextHandlerOptions } from '@opennextjs/aws/types/overrides.js';
3
+
4
+ /**
5
+ * Azure Image Optimization Handler
6
+ *
7
+ * Wraps OpenNext's default image optimization handler with Azure Blob caching.
8
+ *
9
+ * When AZURE_IMAGE_OPTIMIZATION_CACHE=true:
10
+ * - First request: Loads from "assets", processes, caches in "optimized-images", returns
11
+ * - Subsequent requests: Returns from "optimized-images" cache (no processing)
12
+ *
13
+ * When AZURE_IMAGE_OPTIMIZATION_CACHE=false:
14
+ * - Every request processes the image (no caching)
15
+ */
16
+ declare function createImageOptimizationHandler(): Promise<OpenNextHandler<InternalEvent, InternalResult>>;
17
+ declare const handler: OpenNextHandler<InternalEvent, InternalResult>;
18
+ declare function defaultHandler(event: InternalEvent, options?: OpenNextHandlerOptions): Promise<InternalResult>;
19
+
20
+ export { createImageOptimizationHandler, defaultHandler, handler };
@@ -0,0 +1,20 @@
1
+ import { InternalEvent, InternalResult } from '@opennextjs/aws/types/open-next.js';
2
+ import { OpenNextHandler, OpenNextHandlerOptions } from '@opennextjs/aws/types/overrides.js';
3
+
4
+ /**
5
+ * Azure Image Optimization Handler
6
+ *
7
+ * Wraps OpenNext's default image optimization handler with Azure Blob caching.
8
+ *
9
+ * When AZURE_IMAGE_OPTIMIZATION_CACHE=true:
10
+ * - First request: Loads from "assets", processes, caches in "optimized-images", returns
11
+ * - Subsequent requests: Returns from "optimized-images" cache (no processing)
12
+ *
13
+ * When AZURE_IMAGE_OPTIMIZATION_CACHE=false:
14
+ * - Every request processes the image (no caching)
15
+ */
16
+ declare function createImageOptimizationHandler(): Promise<OpenNextHandler<InternalEvent, InternalResult>>;
17
+ declare const handler: OpenNextHandler<InternalEvent, InternalResult>;
18
+ declare function defaultHandler(event: InternalEvent, options?: OpenNextHandlerOptions): Promise<InternalResult>;
19
+
20
+ export { createImageOptimizationHandler, defaultHandler, handler };
@@ -0,0 +1,11 @@
1
+ async function createImageOptimizationHandler() {
2
+ const { defaultHandler: defaultHandler2 } = await import('@opennextjs/aws/adapters/image-optimization-adapter.js');
3
+ const { createCachedImageOptimizationHandler } = await import('../overrides/imageOptimization/azure-cached.js');
4
+ return createCachedImageOptimizationHandler(defaultHandler2);
5
+ }
6
+ const handler = await createImageOptimizationHandler();
7
+ async function defaultHandler(event, options) {
8
+ return handler(event, options);
9
+ }
10
+
11
+ export { createImageOptimizationHandler, defaultHandler, handler };
@@ -3,7 +3,6 @@ import { Writable } from 'node:stream';
3
3
  const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 204, 205, 304]);
4
4
  const STATIC_ASSET_PATTERNS = [
5
5
  /^\/_next\/static\//,
6
- /^\/_next\/data\//,
7
6
  /^\/favicon\.ico$/,
8
7
  /^\/robots\.txt$/,
9
8
  /^\/sitemap\.xml$/,
@@ -17,11 +16,12 @@ const handler = async (handler2, converter) => async (context, request) => {
17
16
  const internalEvent = await converter.convertFrom(request);
18
17
  if (isStaticAssetRequest(internalEvent.rawPath)) {
19
18
  const blobUrl = `https://${process.env.AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net/assets${internalEvent.rawPath}`;
19
+ const cacheControl = internalEvent.rawPath.startsWith("/_next/static/") ? "public, max-age=31536000, immutable" : "public, max-age=0, must-revalidate";
20
20
  context.res = {
21
21
  status: 301,
22
22
  headers: {
23
23
  Location: blobUrl,
24
- "Cache-Control": "public, max-age=31536000, immutable"
24
+ "Cache-Control": cacheControl
25
25
  }
26
26
  };
27
27
  return;
@@ -82,13 +82,16 @@ const handler = async (handler2, converter) => async (context, request) => {
82
82
  };
83
83
  }
84
84
  } catch (error) {
85
+ const isProduction = process.env.NODE_ENV === "production";
85
86
  context.res = {
86
87
  status: 500,
87
88
  headers: { "content-type": "application/json" },
88
89
  body: JSON.stringify({
89
90
  error: "Internal Server Error",
90
- message: error instanceof Error ? error.message : String(error),
91
- stack: error instanceof Error ? error.stack : void 0
91
+ ...isProduction ? {} : {
92
+ message: error instanceof Error ? error.message : String(error),
93
+ stack: error instanceof Error ? error.stack : void 0
94
+ }
92
95
  })
93
96
  };
94
97
  }
@@ -0,0 +1,10 @@
1
+ import { InternalEvent, InternalResult } from '@opennextjs/aws/types/open-next.js';
2
+ import { WrapperHandler } from '@opennextjs/aws/types/overrides.js';
3
+
4
+ declare const _default: {
5
+ name: string;
6
+ wrapper: WrapperHandler<InternalEvent, InternalResult>;
7
+ supportStreaming: boolean;
8
+ };
9
+
10
+ export { _default as default };
@@ -0,0 +1,10 @@
1
+ import { InternalEvent, InternalResult } from '@opennextjs/aws/types/open-next.js';
2
+ import { WrapperHandler } from '@opennextjs/aws/types/overrides.js';
3
+
4
+ declare const _default: {
5
+ name: string;
6
+ wrapper: WrapperHandler<InternalEvent, InternalResult>;
7
+ supportStreaming: boolean;
8
+ };
9
+
10
+ export { _default as default };
@@ -0,0 +1,151 @@
1
+ import { Writable } from 'node:stream';
2
+ import { createHash } from 'node:crypto';
3
+ import { BlobServiceClient } from '@azure/storage-blob';
4
+
5
+ const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 204, 205, 304]);
6
+ const { AZURE_STORAGE_CONNECTION_STRING, AZURE_STORAGE_ACCOUNT_NAME } = process.env;
7
+ const CACHE_CONTAINER = "optimized-images";
8
+ function getBlobClient(key) {
9
+ if (!AZURE_STORAGE_CONNECTION_STRING && !AZURE_STORAGE_ACCOUNT_NAME) {
10
+ throw new Error("Azure Storage connection string or account name must be defined");
11
+ }
12
+ const blobServiceClient = AZURE_STORAGE_CONNECTION_STRING ? BlobServiceClient.fromConnectionString(AZURE_STORAGE_CONNECTION_STRING) : new BlobServiceClient(`https://${AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net`);
13
+ const containerClient = blobServiceClient.getContainerClient(CACHE_CONTAINER);
14
+ return containerClient.getBlockBlobClient(key);
15
+ }
16
+ function computeCacheKey(event) {
17
+ const { query } = event;
18
+ const url = Array.isArray(query?.url) ? query.url[0] : query?.url || "";
19
+ const width = Array.isArray(query?.w) ? query.w[0] : query?.w || "0";
20
+ const quality = Array.isArray(query?.q) ? query.q[0] : query?.q || "75";
21
+ const hash = createHash("sha256").update(url).digest("hex").substring(0, 16);
22
+ return `${hash}/w${width}_q${quality}.cache`;
23
+ }
24
+ async function getCachedImage(cacheKey) {
25
+ try {
26
+ const blobClient = getBlobClient(cacheKey);
27
+ const exists = await blobClient.exists();
28
+ if (!exists) {
29
+ return null;
30
+ }
31
+ const downloadResponse = await blobClient.download();
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
+ return Buffer.concat(chunks);
40
+ } catch (error) {
41
+ return null;
42
+ }
43
+ }
44
+ async function setCachedImage(cacheKey, buffer, contentType) {
45
+ try {
46
+ const blobClient = getBlobClient(cacheKey);
47
+ await blobClient.upload(buffer, buffer.length, {
48
+ blobHTTPHeaders: {
49
+ blobContentType: contentType,
50
+ blobCacheControl: "public,max-age=31536000,immutable"
51
+ }
52
+ });
53
+ process.stderr.write(`[ImageCache] \u2713 Cached ${buffer.length} bytes
54
+ `);
55
+ } catch (error) {
56
+ process.stderr.write(`[ImageCache] \u2717 Cache failed: ${error.message}
57
+ `);
58
+ }
59
+ }
60
+ const handler = async (handler2, converter) => async (context, request) => {
61
+ try {
62
+ const internalEvent = await converter.convertFrom(request);
63
+ const cacheKey = computeCacheKey(internalEvent);
64
+ const cachedBuffer = await getCachedImage(cacheKey);
65
+ if (cachedBuffer) {
66
+ process.stderr.write(`[ImageCache] \u2713 Cache HIT
67
+ `);
68
+ context.res = {
69
+ status: 200,
70
+ headers: {
71
+ "Content-Type": "image/webp",
72
+ "Cache-Control": "public,max-age=31536000,immutable"
73
+ },
74
+ body: cachedBuffer
75
+ };
76
+ return;
77
+ }
78
+ process.stderr.write(`[ImageCache] Cache MISS - processing...
79
+ `);
80
+ let streamFinished = null;
81
+ let resolveStream = null;
82
+ let processedBuffer = null;
83
+ let responseContentType = "image/webp";
84
+ const streamCreator = {
85
+ writeHeaders(prelude) {
86
+ const { statusCode, cookies, headers } = prelude;
87
+ responseContentType = headers["Content-Type"] || headers["content-type"] || "image/webp";
88
+ const responseHeaders = { ...headers };
89
+ if (cookies.length > 0) {
90
+ responseHeaders["set-cookie"] = cookies.join(", ");
91
+ }
92
+ if (NULL_BODY_STATUSES.has(statusCode)) {
93
+ context.res = {
94
+ status: statusCode,
95
+ headers: responseHeaders
96
+ };
97
+ return new Writable({
98
+ write(_chunk, _encoding, callback) {
99
+ callback();
100
+ }
101
+ });
102
+ }
103
+ streamFinished = new Promise((resolve) => {
104
+ resolveStream = resolve;
105
+ });
106
+ const chunks = [];
107
+ const writable = new Writable({
108
+ write(chunk, _encoding, callback) {
109
+ chunks.push(Buffer.from(chunk));
110
+ callback();
111
+ },
112
+ final(callback) {
113
+ const body = Buffer.concat(chunks);
114
+ processedBuffer = body;
115
+ context.res = {
116
+ status: statusCode,
117
+ headers: responseHeaders,
118
+ body
119
+ };
120
+ callback();
121
+ resolveStream?.();
122
+ }
123
+ });
124
+ return writable;
125
+ }
126
+ };
127
+ await handler2(internalEvent, { streamCreator });
128
+ if (streamFinished) {
129
+ await streamFinished;
130
+ }
131
+ if (processedBuffer && responseContentType) {
132
+ await setCachedImage(cacheKey, processedBuffer, responseContentType);
133
+ }
134
+ } catch (error) {
135
+ console.error("Image optimization error:", error);
136
+ context.res = {
137
+ status: 500,
138
+ headers: {
139
+ "Content-Type": "text/plain"
140
+ },
141
+ body: "Internal server error"
142
+ };
143
+ }
144
+ };
145
+ const azureImageOptimization = {
146
+ name: "azure-image-optimization",
147
+ wrapper: handler,
148
+ supportStreaming: true
149
+ };
150
+
151
+ export { azureImageOptimization as default };