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 +24 -2
- package/dist/adapters/image-optimization.d.mts +20 -0
- package/dist/adapters/image-optimization.d.ts +20 -0
- package/dist/adapters/image-optimization.js +11 -0
- package/dist/adapters/wrappers/azure-functions.js +7 -4
- package/dist/adapters/wrappers/azure-image-optimization.d.mts +10 -0
- package/dist/adapters/wrappers/azure-image-optimization.d.ts +10 -0
- package/dist/adapters/wrappers/azure-image-optimization.js +151 -0
- package/dist/cli/index.js +486 -5
- package/dist/config/index.d.mts +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +12 -0
- package/dist/deploy.js +182 -54
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/infrastructure/main.bicep +12 -0
- package/dist/overrides/imageLoader/azure-blob.d.mts +5 -0
- package/dist/overrides/imageLoader/azure-blob.d.ts +5 -0
- package/dist/overrides/imageLoader/azure-blob.js +37 -0
- package/dist/overrides/imageOptimization/azure-cached.d.mts +6 -0
- package/dist/overrides/imageOptimization/azure-cached.d.ts +6 -0
- package/dist/overrides/imageOptimization/azure-cached.js +115 -0
- package/dist/shared/{opennextjs-azure.d619537c.d.mts → opennextjs-azure.279bd73d.d.mts} +3 -1
- package/dist/shared/{opennextjs-azure.d619537c.d.ts → opennextjs-azure.279bd73d.d.ts} +3 -1
- package/infrastructure/main.bicep +12 -0
- package/package.json +2 -3
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
|
[](https://www.npmjs.com/package/opennextjs-azure)
|
|
6
6
|
[](https://www.npmjs.com/package/opennextjs-azure)
|
|
7
7
|
[](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
|
+

|
|
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
|
+

|
|
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":
|
|
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
|
-
|
|
91
|
-
|
|
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 };
|