opennextjs-azure 0.1.2 → 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 +8 -1
- 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 +2 -2
- 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 +38 -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 +157 -52
- 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
|
@@ -6,7 +6,7 @@
|
|
|
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
10
|
|
|
11
11
|

|
|
12
12
|
|
|
@@ -23,6 +23,7 @@ Azure Functions is Microsoft's serverless compute platform—comparable to AWS L
|
|
|
23
23
|
| Next.js Feature | Azure Implementation |
|
|
24
24
|
| -------------------------------------- | --------------------------------------------------------------- |
|
|
25
25
|
| Incremental Static Regeneration | Azure Blob Storage |
|
|
26
|
+
| Image Optimization | Azure Blob Storage with automatic caching |
|
|
26
27
|
| Streaming SSR | Azure Functions with Node.js streams |
|
|
27
28
|
| `revalidateTag()` / `revalidatePath()` | Azure Table Storage + Queue Storage |
|
|
28
29
|
| Fetch caching | Azure Blob Storage with build ID namespacing |
|
|
@@ -139,6 +140,12 @@ Converts between Azure Functions HTTP triggers and Next.js InternalEvent/Interna
|
|
|
139
140
|
- **Tag Cache:** Azure Table Storage maps tags → paths for `revalidateTag()`
|
|
140
141
|
- **Revalidation Queue:** Azure Queue Storage triggers on-demand regeneration
|
|
141
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
|
+
|
|
142
149
|
**Build Process:**
|
|
143
150
|
Uses OpenNext's AWS build with Azure-specific overrides, then adds Azure Functions metadata (`host.json`, `function.json`) for v3 programming model.
|
|
144
151
|
|
|
@@ -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;
|
|
@@ -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 };
|
package/dist/cli/index.js
CHANGED
|
@@ -222,6 +222,27 @@ async function checkStorageAccount(appName, resourceGroup, environment) {
|
|
|
222
222
|
details: `Status: ${account.Status}`
|
|
223
223
|
};
|
|
224
224
|
}
|
|
225
|
+
try {
|
|
226
|
+
const { stdout: staticFileStdout } = await execAsync$1(
|
|
227
|
+
`az storage blob list --account-name ${account.Name} --container-name assets --prefix _next/static --query '[0].name' -o tsv --only-show-errors 2>/dev/null || echo ""`
|
|
228
|
+
);
|
|
229
|
+
const sampleFile = staticFileStdout.trim();
|
|
230
|
+
if (sampleFile) {
|
|
231
|
+
const { stdout: cacheStdout } = await execAsync$1(
|
|
232
|
+
`az storage blob show --account-name ${account.Name} --container-name assets --name '${sampleFile}' --query 'properties.contentSettings.cacheControl' -o tsv --only-show-errors 2>/dev/null || echo ""`
|
|
233
|
+
);
|
|
234
|
+
const cacheControl = cacheStdout.trim();
|
|
235
|
+
const expectedCache = "public, max-age=31536000, immutable";
|
|
236
|
+
if (cacheControl !== expectedCache) {
|
|
237
|
+
return {
|
|
238
|
+
passed: false,
|
|
239
|
+
message: "Static asset cache headers misconfigured",
|
|
240
|
+
details: `${sampleFile.split("/").pop()} has '${cacheControl || "none"}' (expected '${expectedCache}')`
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
225
246
|
return {
|
|
226
247
|
passed: true,
|
|
227
248
|
message: "Storage account healthy",
|
|
@@ -296,11 +317,23 @@ async function checkFunctionAppStatus(appName, resourceGroup, environment) {
|
|
|
296
317
|
details: `State: ${funcApp.State}`
|
|
297
318
|
};
|
|
298
319
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
320
|
+
const url = `https://${funcApp.DefaultHostName}`;
|
|
321
|
+
try {
|
|
322
|
+
const startTime = Date.now();
|
|
323
|
+
await execAsync$1(`curl -I -s -o /dev/null -w "%{http_code}" --max-time 10 "${url}" || echo "000"`);
|
|
324
|
+
const responseTime = Date.now() - startTime;
|
|
325
|
+
return {
|
|
326
|
+
passed: true,
|
|
327
|
+
message: "Function app running and responding",
|
|
328
|
+
details: `URL: ${url} (${responseTime}ms)`
|
|
329
|
+
};
|
|
330
|
+
} catch {
|
|
331
|
+
return {
|
|
332
|
+
passed: true,
|
|
333
|
+
message: "Function app running",
|
|
334
|
+
details: `URL: ${url} (HTTP check failed, but app is running)`
|
|
335
|
+
};
|
|
336
|
+
}
|
|
304
337
|
} catch (error) {
|
|
305
338
|
return {
|
|
306
339
|
passed: false,
|
package/dist/config/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@opennextjs/aws/types/open-next.js';
|
|
2
|
-
export { d as defineAzureConfig, g as getAzureConfig } from '../shared/opennextjs-azure.
|
|
2
|
+
export { d as defineAzureConfig, g as getAzureConfig } from '../shared/opennextjs-azure.279bd73d.mjs';
|
|
3
3
|
import '@opennextjs/aws/types/overrides.js';
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@opennextjs/aws/types/open-next.js';
|
|
2
|
-
export { d as defineAzureConfig, g as getAzureConfig } from '../shared/opennextjs-azure.
|
|
2
|
+
export { d as defineAzureConfig, g as getAzureConfig } from '../shared/opennextjs-azure.279bd73d.js';
|
|
3
3
|
import '@opennextjs/aws/types/overrides.js';
|
package/dist/config/index.js
CHANGED
|
@@ -14,6 +14,9 @@ function defineAzureConfig(config = {}) {
|
|
|
14
14
|
middleware: config.middleware || {
|
|
15
15
|
external: false
|
|
16
16
|
},
|
|
17
|
+
imageOptimization: {
|
|
18
|
+
loader: resolveImageLoader(config.imageLoader)
|
|
19
|
+
},
|
|
17
20
|
dangerous: config.dangerous,
|
|
18
21
|
buildCommand: config.buildCommand,
|
|
19
22
|
buildOutputPath: config.buildOutputPath || ".",
|
|
@@ -48,6 +51,15 @@ function resolveQueue(value) {
|
|
|
48
51
|
}
|
|
49
52
|
return () => value;
|
|
50
53
|
}
|
|
54
|
+
function resolveImageLoader(value) {
|
|
55
|
+
if (!value || value === "azure-blob") {
|
|
56
|
+
return () => import('../overrides/imageLoader/azure-blob.js').then((m) => m.default);
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "function") {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
return () => value;
|
|
62
|
+
}
|
|
51
63
|
function getAzureConfig() {
|
|
52
64
|
return {
|
|
53
65
|
deployment: {
|
package/dist/deploy.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { exec } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { build as build$1 } from '@opennextjs/aws/build.js';
|
|
7
|
-
import fs$1 from 'node:fs';
|
|
7
|
+
import fs$1, { existsSync } from 'node:fs';
|
|
8
8
|
import readline from 'node:readline';
|
|
9
9
|
|
|
10
10
|
const execAsync$3 = promisify(exec);
|
|
@@ -61,6 +61,42 @@ async function scaffoldProject(targetDir, options = {}) {
|
|
|
61
61
|
});
|
|
62
62
|
await execAsync$3("pnpm install", { cwd: targetDir });
|
|
63
63
|
console.log("Dependencies installed\n");
|
|
64
|
+
const pagePath = path.join(targetDir, srcDir ? "src/app/page.tsx" : "app/page.tsx");
|
|
65
|
+
let pageContent = await fs.readFile(pagePath, "utf-8");
|
|
66
|
+
pageContent = pageContent.replace(
|
|
67
|
+
/<Image className="dark:invert" src="\/next\.svg" alt="Next\.js logo" width=\{180\} height=\{38\} priority \/>/,
|
|
68
|
+
`<div className="flex items-center gap-4">
|
|
69
|
+
<Image className="dark:invert" src="/next.svg" alt="Next.js logo" width={180} height={38} priority />
|
|
70
|
+
<span className="text-2xl text-gray-400 dark:text-gray-600">+</span>
|
|
71
|
+
<Image src="/azure.png" alt="Azure logo" width={38} height={38} priority />
|
|
72
|
+
</div>`
|
|
73
|
+
);
|
|
74
|
+
pageContent = pageContent.replace(
|
|
75
|
+
/href="https:\/\/vercel\.com\/new[^"]*"/,
|
|
76
|
+
'href="https://github.com/zpg6/opennextjs-azure"'
|
|
77
|
+
);
|
|
78
|
+
pageContent = pageContent.replace(
|
|
79
|
+
/className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background/,
|
|
80
|
+
'className="rounded-full border border-solid border-blue-400 transition-colors flex items-center justify-center bg-blue-500/10'
|
|
81
|
+
);
|
|
82
|
+
pageContent = pageContent.replace(/hover:bg-\[#383838\] dark:hover:bg-\[#ccc\]/, "hover:bg-blue-500/20");
|
|
83
|
+
pageContent = pageContent.replace(
|
|
84
|
+
/<Image className="dark:invert" src="\/vercel\.svg" alt="Vercel logomark" width=\{20\} height=\{20\} \/>/,
|
|
85
|
+
'<Image src="/azure.png" alt="Azure logomark" width={20} height={20} />'
|
|
86
|
+
);
|
|
87
|
+
pageContent = pageContent.replace(/>Deploy now</, ">Deploy to Azure<");
|
|
88
|
+
await fs.writeFile(pagePath, pageContent);
|
|
89
|
+
const publicDir = path.join(targetDir, "public");
|
|
90
|
+
const azureLogoSource = path.join(
|
|
91
|
+
path.dirname(new URL(import.meta.url).pathname),
|
|
92
|
+
"../../examples/basic-app/public/azure.png"
|
|
93
|
+
);
|
|
94
|
+
const azureLogoDest = path.join(publicDir, "azure.png");
|
|
95
|
+
try {
|
|
96
|
+
await fs.copyFile(azureLogoSource, azureLogoDest);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.warn("Warning: Could not copy Azure logo. You can add it manually to public/azure.png");
|
|
99
|
+
}
|
|
64
100
|
console.log("Creating open-next.config.ts...");
|
|
65
101
|
const openNextConfig = `// @ts-nocheck
|
|
66
102
|
export default {
|
|
@@ -78,6 +114,17 @@ export default {
|
|
|
78
114
|
middleware: {
|
|
79
115
|
external: false,
|
|
80
116
|
},
|
|
117
|
+
imageOptimization: {
|
|
118
|
+
loader: () => import("./node_modules/opennextjs-azure/dist/overrides/imageLoader/azure-blob.js").then(m => m.default),
|
|
119
|
+
override: {
|
|
120
|
+
wrapper: () => import("./node_modules/opennextjs-azure/dist/adapters/wrappers/azure-image-optimization.js").then(m => m.default),
|
|
121
|
+
converter: () => import("./node_modules/opennextjs-azure/dist/adapters/converters/azure-http.js").then(m => m.default),
|
|
122
|
+
},
|
|
123
|
+
install: {
|
|
124
|
+
packages: ["@img/sharp-linux-x64@0.33.5", "sharp@0.33.5"],
|
|
125
|
+
additionalArgs: "--force --ignore-scripts",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
81
128
|
buildOutputPath: ".",
|
|
82
129
|
appPath: ".",
|
|
83
130
|
};
|
|
@@ -253,8 +300,48 @@ async function prepareFunctions() {
|
|
|
253
300
|
entryPoint: "handler"
|
|
254
301
|
};
|
|
255
302
|
await fs.writeFile(path.join(functionDir, "function.json"), JSON.stringify(functionJson, null, 2));
|
|
256
|
-
|
|
257
|
-
|
|
303
|
+
const imageOptDir = path.join(process.cwd(), ".open-next/image-optimization-function");
|
|
304
|
+
try {
|
|
305
|
+
await fs.access(imageOptDir);
|
|
306
|
+
console.log(" Adding image optimization function...");
|
|
307
|
+
const imageFunctionDir = path.join(functionsDir, "image-optimization");
|
|
308
|
+
await fs.mkdir(imageFunctionDir, { recursive: true });
|
|
309
|
+
const imageFunctionJson = {
|
|
310
|
+
bindings: [
|
|
311
|
+
{
|
|
312
|
+
authLevel: "anonymous",
|
|
313
|
+
type: "httpTrigger",
|
|
314
|
+
direction: "in",
|
|
315
|
+
name: "req",
|
|
316
|
+
methods: ["get", "head"],
|
|
317
|
+
route: "_next/image"
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
type: "http",
|
|
321
|
+
direction: "out",
|
|
322
|
+
name: "res"
|
|
323
|
+
}
|
|
324
|
+
],
|
|
325
|
+
scriptFile: "../index-image.mjs",
|
|
326
|
+
entryPoint: "handler"
|
|
327
|
+
};
|
|
328
|
+
await fs.writeFile(path.join(imageFunctionDir, "function.json"), JSON.stringify(imageFunctionJson, null, 2));
|
|
329
|
+
await fs.copyFile(path.join(imageOptDir, "index.mjs"), path.join(functionsDir, "index-image.mjs"));
|
|
330
|
+
await fs.cp(path.join(imageOptDir, ".next"), path.join(functionsDir, ".next"), {
|
|
331
|
+
recursive: true,
|
|
332
|
+
force: false
|
|
333
|
+
});
|
|
334
|
+
try {
|
|
335
|
+
await fs.copyFile(
|
|
336
|
+
path.join(imageOptDir, "open-next.config.mjs"),
|
|
337
|
+
path.join(functionsDir, "open-next.config.mjs")
|
|
338
|
+
);
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
console.log(` ${greenCheck()} Image optimization function added`);
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
console.log(` ${greenCheck()} Azure Functions metadata created`);
|
|
258
345
|
console.log("Installing minimal runtime dependencies...");
|
|
259
346
|
try {
|
|
260
347
|
const originalPackageJson = JSON.parse(await fs.readFile(path.join(functionsDir, "package.json"), "utf-8"));
|
|
@@ -274,23 +361,33 @@ async function prepareFunctions() {
|
|
|
274
361
|
await execAsync$2("npm install --production --no-package-lock --loglevel=error", {
|
|
275
362
|
cwd: functionsDir
|
|
276
363
|
});
|
|
277
|
-
console.log(` ${greenCheck()} Runtime dependencies installed
|
|
278
|
-
`);
|
|
364
|
+
console.log(` ${greenCheck()} Runtime dependencies installed`);
|
|
279
365
|
} catch (error) {
|
|
280
366
|
console.error("Failed to install dependencies:", error.message);
|
|
281
367
|
throw error;
|
|
282
368
|
}
|
|
369
|
+
const imageOptDir2 = path.join(process.cwd(), ".open-next/image-optimization-function");
|
|
370
|
+
try {
|
|
371
|
+
await fs.access(imageOptDir2);
|
|
372
|
+
console.log("Installing Sharp with Linux x64 binaries for image optimization...");
|
|
373
|
+
await execAsync$2(
|
|
374
|
+
"npm install --force sharp@0.33.5 @img/sharp-linux-x64@0.33.5 @img/sharp-libvips-linux-x64@1.0.4",
|
|
375
|
+
{ cwd: functionsDir }
|
|
376
|
+
);
|
|
377
|
+
console.log(` ${greenCheck()} Sharp with Linux x64 binaries installed`);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
}
|
|
283
380
|
}
|
|
284
381
|
|
|
285
382
|
async function build(configPath) {
|
|
286
|
-
console.log("Building Next.js app for Azure
|
|
383
|
+
console.log("Building Next.js app for Azure...");
|
|
287
384
|
const baseDir = process.cwd();
|
|
288
385
|
const userConfigPath = configPath || "open-next.config.ts";
|
|
289
386
|
const absoluteUserConfigPath = path.join(baseDir, userConfigPath);
|
|
290
387
|
let resolvedConfigPath = userConfigPath;
|
|
291
388
|
let tempConfigPath = null;
|
|
292
389
|
if (!fs$1.existsSync(absoluteUserConfigPath)) {
|
|
293
|
-
console.log("No open-next.config.ts found, using default Azure configuration
|
|
390
|
+
console.log("No open-next.config.ts found, using default Azure configuration");
|
|
294
391
|
const { createRequire } = await import('node:module');
|
|
295
392
|
const require = createRequire(import.meta.url);
|
|
296
393
|
const packagePath = path.dirname(require.resolve("opennextjs-azure/package.json"));
|
|
@@ -328,20 +425,17 @@ export default {
|
|
|
328
425
|
if (fs$1.existsSync(openNextPath)) {
|
|
329
426
|
console.log("Cleaning previous build output...");
|
|
330
427
|
fs$1.rmSync(openNextPath, { recursive: true, force: true });
|
|
331
|
-
console.log(` ${greenCheck()} Previous build cleaned
|
|
332
|
-
`);
|
|
428
|
+
console.log(` ${greenCheck()} Previous build cleaned`);
|
|
333
429
|
}
|
|
334
430
|
console.log("Running OpenNext build...");
|
|
335
431
|
const externals = ["@opennextjs/aws"].join(",");
|
|
336
432
|
await build$1(resolvedConfigPath, externals);
|
|
337
|
-
console.log(` ${greenCheck()} OpenNext build complete
|
|
338
|
-
`);
|
|
339
433
|
await prepareFunctions();
|
|
340
434
|
console.log("Build completed successfully!");
|
|
341
|
-
console.log("
|
|
435
|
+
console.log("Output: .open-next/");
|
|
342
436
|
console.log(" \u251C\u2500\u2500 server-functions/default (Azure Functions app)");
|
|
343
|
-
console.log(" \u2514\u2500\u2500 assets (Static files)
|
|
344
|
-
console.log("Next: opennextjs-azure deploy
|
|
437
|
+
console.log(" \u2514\u2500\u2500 assets (Static files)");
|
|
438
|
+
console.log("Next: opennextjs-azure deploy");
|
|
345
439
|
} catch (error) {
|
|
346
440
|
console.error("Build failed:", error);
|
|
347
441
|
process.exit(1);
|
|
@@ -367,8 +461,7 @@ async function deploy$1(options) {
|
|
|
367
461
|
environment = "dev",
|
|
368
462
|
skipInfrastructure = false
|
|
369
463
|
} = options;
|
|
370
|
-
console.log(`Deploying ${appName} to Azure (${environment} environment)
|
|
371
|
-
`);
|
|
464
|
+
console.log(`Deploying ${appName} to Azure (${environment} environment)`);
|
|
372
465
|
try {
|
|
373
466
|
await checkAzureCLI();
|
|
374
467
|
await checkAzureLogin();
|
|
@@ -386,8 +479,7 @@ async function deploy$1(options) {
|
|
|
386
479
|
console.log("Provisioning Azure infrastructure...");
|
|
387
480
|
console.log(` Resource Group: ${resourceGroup}`);
|
|
388
481
|
console.log(` Location: ${location}`);
|
|
389
|
-
console.log(` Environment: ${environment}
|
|
390
|
-
`);
|
|
482
|
+
console.log(` Environment: ${environment}`);
|
|
391
483
|
deploymentOutputs = await provisionInfrastructure({
|
|
392
484
|
appName,
|
|
393
485
|
resourceGroup,
|
|
@@ -395,20 +487,17 @@ async function deploy$1(options) {
|
|
|
395
487
|
environment,
|
|
396
488
|
applicationInsights: options.applicationInsights ?? false
|
|
397
489
|
});
|
|
398
|
-
console.log(` ${greenCheck()} Infrastructure ready
|
|
399
|
-
`);
|
|
490
|
+
console.log(` ${greenCheck()} Infrastructure ready`);
|
|
400
491
|
} else {
|
|
401
|
-
console.log("Skipping infrastructure provisioning
|
|
492
|
+
console.log("Skipping infrastructure provisioning");
|
|
402
493
|
}
|
|
403
494
|
console.log("Uploading static assets...");
|
|
404
495
|
await uploadStaticAssets(appName, resourceGroup);
|
|
405
|
-
console.log(` ${greenCheck()} Assets uploaded
|
|
406
|
-
`);
|
|
496
|
+
console.log(` ${greenCheck()} Assets uploaded`);
|
|
407
497
|
console.log("Deploying Function App...");
|
|
408
498
|
const functionAppName = deploymentOutputs?.functionApp || `${appName}-func-${environment}`;
|
|
409
499
|
await deployFunctionApp(functionAppName, resourceGroup);
|
|
410
|
-
console.log(` ${greenCheck()} Function App deployed
|
|
411
|
-
`);
|
|
500
|
+
console.log(` ${greenCheck()} Function App deployed`);
|
|
412
501
|
await performPostflightChecks(
|
|
413
502
|
resourceGroup,
|
|
414
503
|
functionAppName,
|
|
@@ -438,11 +527,17 @@ async function checkAzureLogin() {
|
|
|
438
527
|
}
|
|
439
528
|
}
|
|
440
529
|
async function checkRequiredProviders(applicationInsights) {
|
|
441
|
-
const requiredProviders = [
|
|
530
|
+
const requiredProviders = [
|
|
531
|
+
"Microsoft.Web",
|
|
532
|
+
"Microsoft.Storage",
|
|
533
|
+
"Microsoft.Compute",
|
|
534
|
+
"Microsoft.Quota",
|
|
535
|
+
"Microsoft.ServiceLinker"
|
|
536
|
+
];
|
|
442
537
|
if (applicationInsights) {
|
|
443
538
|
requiredProviders.push("Microsoft.AlertsManagement");
|
|
444
539
|
}
|
|
445
|
-
console.log("Checking Azure resource providers
|
|
540
|
+
console.log("Checking Azure resource providers...");
|
|
446
541
|
for (const provider of requiredProviders) {
|
|
447
542
|
const { stdout } = await execAsync$1(
|
|
448
543
|
`az provider show --namespace ${provider} --query "registrationState" -o tsv`
|
|
@@ -451,8 +546,7 @@ async function checkRequiredProviders(applicationInsights) {
|
|
|
451
546
|
if (state !== "Registered") {
|
|
452
547
|
console.log(`Registering ${provider}...`);
|
|
453
548
|
await execAsync$1(`az provider register --namespace ${provider} --wait`);
|
|
454
|
-
console.log(` ${greenCheck()} ${provider} registered
|
|
455
|
-
`);
|
|
549
|
+
console.log(` ${greenCheck()} ${provider} registered`);
|
|
456
550
|
}
|
|
457
551
|
}
|
|
458
552
|
}
|
|
@@ -480,16 +574,13 @@ async function checkQuotaAvailability(location, environment) {
|
|
|
480
574
|
console.error(`
|
|
481
575
|
${redX()} Quota Error: No quota available for ${environment} environment`);
|
|
482
576
|
console.error(` Required: ${requiredSku.name}`);
|
|
483
|
-
console.error(` Current Limit: ${requiredSku.quota}
|
|
484
|
-
`);
|
|
577
|
+
console.error(` Current Limit: ${requiredSku.quota}`);
|
|
485
578
|
if (y1Limit > 0 && environment !== "dev") {
|
|
486
579
|
console.log(` Suggestion: Deploy to dev environment instead (has quota: ${y1Limit})`);
|
|
487
|
-
console.log(` Command: opennextjs-azure deploy --environment dev
|
|
488
|
-
`);
|
|
580
|
+
console.log(` Command: opennextjs-azure deploy --environment dev`);
|
|
489
581
|
} else if (ep1Limit > 0 && environment === "dev") {
|
|
490
582
|
console.log(` Suggestion: Deploy to prod environment instead (has quota: ${ep1Limit})`);
|
|
491
|
-
console.log(` Command: opennextjs-azure deploy --environment prod
|
|
492
|
-
`);
|
|
583
|
+
console.log(` Command: opennextjs-azure deploy --environment prod`);
|
|
493
584
|
} else {
|
|
494
585
|
console.log(` To request quota increase:`);
|
|
495
586
|
console.log(
|
|
@@ -502,16 +593,13 @@ ${redX()} Quota Error: No quota available for ${environment} environment`);
|
|
|
502
593
|
}
|
|
503
594
|
throw new Error(`No ${requiredSku.type} quota available for ${environment} environment in ${location}`);
|
|
504
595
|
}
|
|
505
|
-
console.log(` ${greenCheck()} ${requiredSku.name}: ${requiredSku.quota} instances available
|
|
506
|
-
`);
|
|
596
|
+
console.log(` ${greenCheck()} ${requiredSku.name}: ${requiredSku.quota} instances available`);
|
|
507
597
|
if (environment === "dev" && ep1Limit > 0) {
|
|
508
598
|
console.log(
|
|
509
|
-
` Premium tier also available (${ep1Limit} instances) - use --environment prod for better performance
|
|
510
|
-
`
|
|
599
|
+
` Premium tier also available (${ep1Limit} instances) - use --environment prod for better performance`
|
|
511
600
|
);
|
|
512
601
|
} else if (environment !== "dev" && y1Limit > 0) {
|
|
513
|
-
console.log(` Consumption tier available (${y1Limit} instances) - use --environment dev for lower cost
|
|
514
|
-
`);
|
|
602
|
+
console.log(` Consumption tier available (${y1Limit} instances) - use --environment dev for lower cost`);
|
|
515
603
|
}
|
|
516
604
|
} catch (error) {
|
|
517
605
|
if (error.message?.includes("No") && error.message?.includes("quota available")) {
|
|
@@ -552,8 +640,7 @@ async function checkLocation(location) {
|
|
|
552
640
|
Available regions: ${available.trim().split("\n").slice(0, 10).join(", ")}`
|
|
553
641
|
);
|
|
554
642
|
}
|
|
555
|
-
console.log(` ${greenCheck()} Region: ${locations[0].DisplayName} (${location})
|
|
556
|
-
`);
|
|
643
|
+
console.log(` ${greenCheck()} Region: ${locations[0].DisplayName} (${location})`);
|
|
557
644
|
} catch (error) {
|
|
558
645
|
if (error.message.includes("Invalid location")) {
|
|
559
646
|
throw error;
|
|
@@ -584,8 +671,7 @@ Run 'opennextjs-azure build' to regenerate`
|
|
|
584
671
|
);
|
|
585
672
|
}
|
|
586
673
|
}
|
|
587
|
-
console.log(` ${greenCheck()} Build output structure valid
|
|
588
|
-
`);
|
|
674
|
+
console.log(` ${greenCheck()} Build output structure valid`);
|
|
589
675
|
}
|
|
590
676
|
async function checkExistingInfrastructure(appName, resourceGroup, environment) {
|
|
591
677
|
console.log("Checking existing infrastructure...");
|
|
@@ -641,16 +727,16 @@ async function performPostflightChecks(resourceGroup, functionAppName, location,
|
|
|
641
727
|
console.log("\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
642
728
|
console.log(`${greenCheck()} Deployment Complete!`);
|
|
643
729
|
console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
644
|
-
console.log("
|
|
730
|
+
console.log("Application:");
|
|
645
731
|
console.log(` App URL: ${functionUrl}`);
|
|
646
732
|
console.log(` Assets URL: ${assetsUrl}`);
|
|
647
733
|
console.log(` Status: ${funcApp.State}`);
|
|
648
734
|
console.log(` Type: ${funcApp.Kind}`);
|
|
649
|
-
console.log("
|
|
735
|
+
console.log("Infrastructure:");
|
|
650
736
|
console.log(` Resource Group: ${resourceGroup}`);
|
|
651
737
|
console.log(` Region: ${location}`);
|
|
652
738
|
console.log(` Environment: ${environment}`);
|
|
653
|
-
console.log("
|
|
739
|
+
console.log("Configuration:");
|
|
654
740
|
console.log(` App Service Plan: ${plan.Tier} (${plan.Sku})`);
|
|
655
741
|
console.log(` Storage Account: ${storage.Name} (${storage.Sku})`);
|
|
656
742
|
console.log(` Capacity: ${plan.Capacity || 1} instance(s)`);
|
|
@@ -665,9 +751,7 @@ async function performPostflightChecks(resourceGroup, functionAppName, location,
|
|
|
665
751
|
}
|
|
666
752
|
}
|
|
667
753
|
console.log("\nQuick Actions:");
|
|
668
|
-
console.log(
|
|
669
|
-
` View logs: az functionapp log tail --name ${functionAppName} --resource-group ${resourceGroup}`
|
|
670
|
-
);
|
|
754
|
+
console.log(` View logs: npx opennextjs-azure@latest tail`);
|
|
671
755
|
console.log(` Open in portal: ${portalUrl}`);
|
|
672
756
|
console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
|
|
673
757
|
if (funcApp.State !== "Running") {
|
|
@@ -705,15 +789,36 @@ async function provisionInfrastructure(options) {
|
|
|
705
789
|
);
|
|
706
790
|
return JSON.parse(stdout);
|
|
707
791
|
}
|
|
792
|
+
async function patchCSSForBlobStorage(assetsPath) {
|
|
793
|
+
const cssPath = path.join(assetsPath, "_next/static/css");
|
|
794
|
+
if (!existsSync(cssPath)) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const files = await fs.readdir(cssPath);
|
|
798
|
+
const cssFiles = files.filter((f) => f.endsWith(".css"));
|
|
799
|
+
for (const file of cssFiles) {
|
|
800
|
+
const filePath = path.join(cssPath, file);
|
|
801
|
+
let content = await fs.readFile(filePath, "utf-8");
|
|
802
|
+
content = content.replace(/url\(\s*(['"]?)(\/_next\/static\/media\/[^'")\s]+)\1\s*\)/g, "url($1/assets$2$1)");
|
|
803
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
804
|
+
}
|
|
805
|
+
}
|
|
708
806
|
async function uploadStaticAssets(appName, resourceGroup) {
|
|
709
807
|
const assetsPath = path.join(process.cwd(), ".open-next/assets");
|
|
808
|
+
await patchCSSForBlobStorage(assetsPath);
|
|
710
809
|
const { stdout } = await execAsync$1(
|
|
711
810
|
`az storage account list --resource-group ${resourceGroup} --query "[0].name" -o tsv`
|
|
712
811
|
);
|
|
713
812
|
const storageAccountName = stdout.trim();
|
|
714
813
|
await execAsync$1(
|
|
715
|
-
`az storage blob upload-batch --account-name ${storageAccountName} --destination assets --source ${assetsPath} --overwrite`
|
|
814
|
+
`az storage blob upload-batch --account-name ${storageAccountName} --destination assets --source ${assetsPath} --content-cache-control "public, max-age=0, must-revalidate" --overwrite`
|
|
716
815
|
);
|
|
816
|
+
const nextStaticPath = path.join(assetsPath, "_next/static");
|
|
817
|
+
if (existsSync(nextStaticPath)) {
|
|
818
|
+
await execAsync$1(
|
|
819
|
+
`az storage blob upload-batch --account-name ${storageAccountName} --destination assets --source ${nextStaticPath} --destination-path _next/static --content-cache-control "public, max-age=31536000, immutable" --overwrite`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
717
822
|
}
|
|
718
823
|
async function deployFunctionApp(functionAppName, resourceGroup) {
|
|
719
824
|
const functionsPath = path.join(process.cwd(), ".open-next/server-functions/default");
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AzureConfig, a as AzureDeploymentTarget, d as defineAzureConfig } from './shared/opennextjs-azure.
|
|
1
|
+
export { A as AzureConfig, a as AzureDeploymentTarget, d as defineAzureConfig } from './shared/opennextjs-azure.279bd73d.mjs';
|
|
2
2
|
export { default as azureBlobCache } from './overrides/incrementalCache/azure-blob.mjs';
|
|
3
3
|
export { default as azureTableTagCache } from './overrides/tagCache/azure-table.mjs';
|
|
4
4
|
export { default as azureQueueRevalidation } from './overrides/queue/azure-queue.mjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AzureConfig, a as AzureDeploymentTarget, d as defineAzureConfig } from './shared/opennextjs-azure.
|
|
1
|
+
export { A as AzureConfig, a as AzureDeploymentTarget, d as defineAzureConfig } from './shared/opennextjs-azure.279bd73d.js';
|
|
2
2
|
export { default as azureBlobCache } from './overrides/incrementalCache/azure-blob.js';
|
|
3
3
|
export { default as azureTableTagCache } from './overrides/tagCache/azure-table.js';
|
|
4
4
|
export { default as azureQueueRevalidation } from './overrides/queue/azure-queue.js';
|
|
@@ -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'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
const { AZURE_STORAGE_ACCOUNT_NAME } = process.env;
|
|
4
|
+
const azureBlobImageLoader = {
|
|
5
|
+
name: "azure-blob",
|
|
6
|
+
load: async (key) => {
|
|
7
|
+
if (!AZURE_STORAGE_ACCOUNT_NAME) {
|
|
8
|
+
throw new Error("AZURE_STORAGE_ACCOUNT_NAME must be defined");
|
|
9
|
+
}
|
|
10
|
+
const cleanKey = key.replace(/^\//, "");
|
|
11
|
+
const blobUrl = `https://${AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net/assets/${cleanKey}`;
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch(blobUrl);
|
|
14
|
+
if (response.status === 404) {
|
|
15
|
+
throw new Error(`Image not found in blob storage: ${cleanKey}`);
|
|
16
|
+
}
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
throw new Error(`Failed to fetch image. Status: ${response.status}`);
|
|
19
|
+
}
|
|
20
|
+
if (!response.body) {
|
|
21
|
+
throw new Error("No body in fetch response");
|
|
22
|
+
}
|
|
23
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
24
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
25
|
+
const body = Readable.from(buffer);
|
|
26
|
+
return {
|
|
27
|
+
body,
|
|
28
|
+
contentType: response.headers.get("content-type") ?? void 0,
|
|
29
|
+
cacheControl: response.headers.get("cache-control") ?? void 0
|
|
30
|
+
};
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(`Failed to load image from Azure Blob: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { azureBlobImageLoader as default };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { InternalEvent, InternalResult } from '@opennextjs/aws/types/open-next.js';
|
|
2
|
+
import { OpenNextHandler } from '@opennextjs/aws/types/overrides.js';
|
|
3
|
+
|
|
4
|
+
declare function createCachedImageOptimizationHandler(defaultHandler: OpenNextHandler<InternalEvent, InternalResult>): OpenNextHandler<InternalEvent, InternalResult>;
|
|
5
|
+
|
|
6
|
+
export { createCachedImageOptimizationHandler };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { InternalEvent, InternalResult } from '@opennextjs/aws/types/open-next.js';
|
|
2
|
+
import { OpenNextHandler } from '@opennextjs/aws/types/overrides.js';
|
|
3
|
+
|
|
4
|
+
declare function createCachedImageOptimizationHandler(defaultHandler: OpenNextHandler<InternalEvent, InternalResult>): OpenNextHandler<InternalEvent, InternalResult>;
|
|
5
|
+
|
|
6
|
+
export { createCachedImageOptimizationHandler };
|
|
@@ -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.
|
|
4
|
-
"description": "
|
|
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",
|