opennextjs-azure 0.1.2 → 0.1.4
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 +39 -6
- 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 +179 -58
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -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 +90 -91
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);
|
|
@@ -365,18 +459,22 @@ async function deploy$1(options) {
|
|
|
365
459
|
resourceGroup = `${appName}-rg`,
|
|
366
460
|
location = "eastus",
|
|
367
461
|
environment = "dev",
|
|
368
|
-
skipInfrastructure = false
|
|
462
|
+
skipInfrastructure = false,
|
|
463
|
+
skipResourceChecks = false
|
|
369
464
|
} = options;
|
|
370
|
-
console.log(`Deploying ${appName} to Azure (${environment} environment)
|
|
371
|
-
`);
|
|
465
|
+
console.log(`Deploying ${appName} to Azure (${environment} environment)`);
|
|
372
466
|
try {
|
|
373
467
|
await checkAzureCLI();
|
|
374
468
|
await checkAzureLogin();
|
|
375
|
-
await checkAzureSubscriptionPermissions();
|
|
376
|
-
await checkLocation(location);
|
|
377
|
-
await checkRequiredProviders(options.applicationInsights);
|
|
378
|
-
await checkQuotaAvailability(location, environment);
|
|
379
469
|
await checkBuildOutput();
|
|
470
|
+
if (!skipResourceChecks) {
|
|
471
|
+
await checkAzureSubscriptionPermissions();
|
|
472
|
+
await checkLocation(location);
|
|
473
|
+
await checkRequiredProviders(options.applicationInsights);
|
|
474
|
+
await checkQuotaAvailability(location, environment);
|
|
475
|
+
} else {
|
|
476
|
+
console.log("Skipping resource checks (--skip-resource-checks)");
|
|
477
|
+
}
|
|
380
478
|
if (skipInfrastructure) {
|
|
381
479
|
await checkExistingInfrastructure(appName, resourceGroup, environment);
|
|
382
480
|
}
|
|
@@ -386,8 +484,7 @@ async function deploy$1(options) {
|
|
|
386
484
|
console.log("Provisioning Azure infrastructure...");
|
|
387
485
|
console.log(` Resource Group: ${resourceGroup}`);
|
|
388
486
|
console.log(` Location: ${location}`);
|
|
389
|
-
console.log(` Environment: ${environment}
|
|
390
|
-
`);
|
|
487
|
+
console.log(` Environment: ${environment}`);
|
|
391
488
|
deploymentOutputs = await provisionInfrastructure({
|
|
392
489
|
appName,
|
|
393
490
|
resourceGroup,
|
|
@@ -395,20 +492,17 @@ async function deploy$1(options) {
|
|
|
395
492
|
environment,
|
|
396
493
|
applicationInsights: options.applicationInsights ?? false
|
|
397
494
|
});
|
|
398
|
-
console.log(` ${greenCheck()} Infrastructure ready
|
|
399
|
-
`);
|
|
495
|
+
console.log(` ${greenCheck()} Infrastructure ready`);
|
|
400
496
|
} else {
|
|
401
|
-
console.log("Skipping infrastructure provisioning
|
|
497
|
+
console.log("Skipping infrastructure provisioning");
|
|
402
498
|
}
|
|
403
499
|
console.log("Uploading static assets...");
|
|
404
500
|
await uploadStaticAssets(appName, resourceGroup);
|
|
405
|
-
console.log(` ${greenCheck()} Assets uploaded
|
|
406
|
-
`);
|
|
501
|
+
console.log(` ${greenCheck()} Assets uploaded`);
|
|
407
502
|
console.log("Deploying Function App...");
|
|
408
503
|
const functionAppName = deploymentOutputs?.functionApp || `${appName}-func-${environment}`;
|
|
409
504
|
await deployFunctionApp(functionAppName, resourceGroup);
|
|
410
|
-
console.log(` ${greenCheck()} Function App deployed
|
|
411
|
-
`);
|
|
505
|
+
console.log(` ${greenCheck()} Function App deployed`);
|
|
412
506
|
await performPostflightChecks(
|
|
413
507
|
resourceGroup,
|
|
414
508
|
functionAppName,
|
|
@@ -438,11 +532,17 @@ async function checkAzureLogin() {
|
|
|
438
532
|
}
|
|
439
533
|
}
|
|
440
534
|
async function checkRequiredProviders(applicationInsights) {
|
|
441
|
-
const requiredProviders = [
|
|
535
|
+
const requiredProviders = [
|
|
536
|
+
"Microsoft.Web",
|
|
537
|
+
"Microsoft.Storage",
|
|
538
|
+
"Microsoft.Compute",
|
|
539
|
+
"Microsoft.Quota",
|
|
540
|
+
"Microsoft.ServiceLinker"
|
|
541
|
+
];
|
|
442
542
|
if (applicationInsights) {
|
|
443
543
|
requiredProviders.push("Microsoft.AlertsManagement");
|
|
444
544
|
}
|
|
445
|
-
console.log("Checking Azure resource providers
|
|
545
|
+
console.log("Checking Azure resource providers...");
|
|
446
546
|
for (const provider of requiredProviders) {
|
|
447
547
|
const { stdout } = await execAsync$1(
|
|
448
548
|
`az provider show --namespace ${provider} --query "registrationState" -o tsv`
|
|
@@ -451,8 +551,7 @@ async function checkRequiredProviders(applicationInsights) {
|
|
|
451
551
|
if (state !== "Registered") {
|
|
452
552
|
console.log(`Registering ${provider}...`);
|
|
453
553
|
await execAsync$1(`az provider register --namespace ${provider} --wait`);
|
|
454
|
-
console.log(` ${greenCheck()} ${provider} registered
|
|
455
|
-
`);
|
|
554
|
+
console.log(` ${greenCheck()} ${provider} registered`);
|
|
456
555
|
}
|
|
457
556
|
}
|
|
458
557
|
}
|
|
@@ -480,16 +579,13 @@ async function checkQuotaAvailability(location, environment) {
|
|
|
480
579
|
console.error(`
|
|
481
580
|
${redX()} Quota Error: No quota available for ${environment} environment`);
|
|
482
581
|
console.error(` Required: ${requiredSku.name}`);
|
|
483
|
-
console.error(` Current Limit: ${requiredSku.quota}
|
|
484
|
-
`);
|
|
582
|
+
console.error(` Current Limit: ${requiredSku.quota}`);
|
|
485
583
|
if (y1Limit > 0 && environment !== "dev") {
|
|
486
584
|
console.log(` Suggestion: Deploy to dev environment instead (has quota: ${y1Limit})`);
|
|
487
|
-
console.log(` Command: opennextjs-azure deploy --environment dev
|
|
488
|
-
`);
|
|
585
|
+
console.log(` Command: opennextjs-azure deploy --environment dev`);
|
|
489
586
|
} else if (ep1Limit > 0 && environment === "dev") {
|
|
490
587
|
console.log(` Suggestion: Deploy to prod environment instead (has quota: ${ep1Limit})`);
|
|
491
|
-
console.log(` Command: opennextjs-azure deploy --environment prod
|
|
492
|
-
`);
|
|
588
|
+
console.log(` Command: opennextjs-azure deploy --environment prod`);
|
|
493
589
|
} else {
|
|
494
590
|
console.log(` To request quota increase:`);
|
|
495
591
|
console.log(
|
|
@@ -502,16 +598,13 @@ ${redX()} Quota Error: No quota available for ${environment} environment`);
|
|
|
502
598
|
}
|
|
503
599
|
throw new Error(`No ${requiredSku.type} quota available for ${environment} environment in ${location}`);
|
|
504
600
|
}
|
|
505
|
-
console.log(` ${greenCheck()} ${requiredSku.name}: ${requiredSku.quota} instances available
|
|
506
|
-
`);
|
|
601
|
+
console.log(` ${greenCheck()} ${requiredSku.name}: ${requiredSku.quota} instances available`);
|
|
507
602
|
if (environment === "dev" && ep1Limit > 0) {
|
|
508
603
|
console.log(
|
|
509
|
-
` Premium tier also available (${ep1Limit} instances) - use --environment prod for better performance
|
|
510
|
-
`
|
|
604
|
+
` Premium tier also available (${ep1Limit} instances) - use --environment prod for better performance`
|
|
511
605
|
);
|
|
512
606
|
} else if (environment !== "dev" && y1Limit > 0) {
|
|
513
|
-
console.log(` Consumption tier available (${y1Limit} instances) - use --environment dev for lower cost
|
|
514
|
-
`);
|
|
607
|
+
console.log(` Consumption tier available (${y1Limit} instances) - use --environment dev for lower cost`);
|
|
515
608
|
}
|
|
516
609
|
} catch (error) {
|
|
517
610
|
if (error.message?.includes("No") && error.message?.includes("quota available")) {
|
|
@@ -552,8 +645,7 @@ async function checkLocation(location) {
|
|
|
552
645
|
Available regions: ${available.trim().split("\n").slice(0, 10).join(", ")}`
|
|
553
646
|
);
|
|
554
647
|
}
|
|
555
|
-
console.log(` ${greenCheck()} Region: ${locations[0].DisplayName} (${location})
|
|
556
|
-
`);
|
|
648
|
+
console.log(` ${greenCheck()} Region: ${locations[0].DisplayName} (${location})`);
|
|
557
649
|
} catch (error) {
|
|
558
650
|
if (error.message.includes("Invalid location")) {
|
|
559
651
|
throw error;
|
|
@@ -584,8 +676,7 @@ Run 'opennextjs-azure build' to regenerate`
|
|
|
584
676
|
);
|
|
585
677
|
}
|
|
586
678
|
}
|
|
587
|
-
console.log(` ${greenCheck()} Build output structure valid
|
|
588
|
-
`);
|
|
679
|
+
console.log(` ${greenCheck()} Build output structure valid`);
|
|
589
680
|
}
|
|
590
681
|
async function checkExistingInfrastructure(appName, resourceGroup, environment) {
|
|
591
682
|
console.log("Checking existing infrastructure...");
|
|
@@ -641,16 +732,16 @@ async function performPostflightChecks(resourceGroup, functionAppName, location,
|
|
|
641
732
|
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
733
|
console.log(`${greenCheck()} Deployment Complete!`);
|
|
643
734
|
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("
|
|
735
|
+
console.log("Application:");
|
|
645
736
|
console.log(` App URL: ${functionUrl}`);
|
|
646
737
|
console.log(` Assets URL: ${assetsUrl}`);
|
|
647
738
|
console.log(` Status: ${funcApp.State}`);
|
|
648
739
|
console.log(` Type: ${funcApp.Kind}`);
|
|
649
|
-
console.log("
|
|
740
|
+
console.log("Infrastructure:");
|
|
650
741
|
console.log(` Resource Group: ${resourceGroup}`);
|
|
651
742
|
console.log(` Region: ${location}`);
|
|
652
743
|
console.log(` Environment: ${environment}`);
|
|
653
|
-
console.log("
|
|
744
|
+
console.log("Configuration:");
|
|
654
745
|
console.log(` App Service Plan: ${plan.Tier} (${plan.Sku})`);
|
|
655
746
|
console.log(` Storage Account: ${storage.Name} (${storage.Sku})`);
|
|
656
747
|
console.log(` Capacity: ${plan.Capacity || 1} instance(s)`);
|
|
@@ -665,9 +756,7 @@ async function performPostflightChecks(resourceGroup, functionAppName, location,
|
|
|
665
756
|
}
|
|
666
757
|
}
|
|
667
758
|
console.log("\nQuick Actions:");
|
|
668
|
-
console.log(
|
|
669
|
-
` View logs: az functionapp log tail --name ${functionAppName} --resource-group ${resourceGroup}`
|
|
670
|
-
);
|
|
759
|
+
console.log(` View logs: npx opennextjs-azure@latest tail`);
|
|
671
760
|
console.log(` Open in portal: ${portalUrl}`);
|
|
672
761
|
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
762
|
if (funcApp.State !== "Running") {
|
|
@@ -697,7 +786,17 @@ async function syncBicepTemplate() {
|
|
|
697
786
|
}
|
|
698
787
|
async function provisionInfrastructure(options) {
|
|
699
788
|
const { appName, resourceGroup, location, environment, applicationInsights } = options;
|
|
700
|
-
await execAsync$1(`az group
|
|
789
|
+
const { stdout: rgExists } = await execAsync$1(`az group exists --name ${resourceGroup}`);
|
|
790
|
+
if (rgExists.trim() === "true") {
|
|
791
|
+
const { stdout: rgLocation } = await execAsync$1(`az group show --name ${resourceGroup} --query location -o tsv`);
|
|
792
|
+
if (rgLocation.trim() !== location) {
|
|
793
|
+
console.warn(
|
|
794
|
+
` ${colors.yellow}Warning:${colors.reset} Resource group "${resourceGroup}" exists in ${rgLocation.trim()}, but ${location} was specified`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
} else {
|
|
798
|
+
await execAsync$1(`az group create --name ${resourceGroup} --location ${location}`);
|
|
799
|
+
}
|
|
701
800
|
const bicepPath = path.join(process.cwd(), "infrastructure/main.bicep");
|
|
702
801
|
const enableAppInsights = applicationInsights ? "true" : "false";
|
|
703
802
|
const { stdout } = await execAsync$1(
|
|
@@ -705,15 +804,36 @@ async function provisionInfrastructure(options) {
|
|
|
705
804
|
);
|
|
706
805
|
return JSON.parse(stdout);
|
|
707
806
|
}
|
|
807
|
+
async function patchCSSForBlobStorage(assetsPath) {
|
|
808
|
+
const cssPath = path.join(assetsPath, "_next/static/css");
|
|
809
|
+
if (!existsSync(cssPath)) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const files = await fs.readdir(cssPath);
|
|
813
|
+
const cssFiles = files.filter((f) => f.endsWith(".css"));
|
|
814
|
+
for (const file of cssFiles) {
|
|
815
|
+
const filePath = path.join(cssPath, file);
|
|
816
|
+
let content = await fs.readFile(filePath, "utf-8");
|
|
817
|
+
content = content.replace(/url\(\s*(['"]?)(\/_next\/static\/media\/[^'")\s]+)\1\s*\)/g, "url($1/assets$2$1)");
|
|
818
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
819
|
+
}
|
|
820
|
+
}
|
|
708
821
|
async function uploadStaticAssets(appName, resourceGroup) {
|
|
709
822
|
const assetsPath = path.join(process.cwd(), ".open-next/assets");
|
|
823
|
+
await patchCSSForBlobStorage(assetsPath);
|
|
710
824
|
const { stdout } = await execAsync$1(
|
|
711
825
|
`az storage account list --resource-group ${resourceGroup} --query "[0].name" -o tsv`
|
|
712
826
|
);
|
|
713
827
|
const storageAccountName = stdout.trim();
|
|
714
828
|
await execAsync$1(
|
|
715
|
-
`az storage blob upload-batch --account-name ${storageAccountName} --destination assets --source ${assetsPath} --overwrite`
|
|
829
|
+
`az storage blob upload-batch --account-name ${storageAccountName} --destination assets --source ${assetsPath} --content-cache-control "public, max-age=0, must-revalidate" --overwrite`
|
|
716
830
|
);
|
|
831
|
+
const nextStaticPath = path.join(assetsPath, "_next/static");
|
|
832
|
+
if (existsSync(nextStaticPath)) {
|
|
833
|
+
await execAsync$1(
|
|
834
|
+
`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`
|
|
835
|
+
);
|
|
836
|
+
}
|
|
717
837
|
}
|
|
718
838
|
async function deployFunctionApp(functionAppName, resourceGroup) {
|
|
719
839
|
const functionsPath = path.join(process.cwd(), ".open-next/server-functions/default");
|
|
@@ -840,6 +960,7 @@ async function deploy(options) {
|
|
|
840
960
|
location: resourceGroupLocation || options.location || config.location || "eastus",
|
|
841
961
|
environment,
|
|
842
962
|
skipInfrastructure: options.skipInfrastructure,
|
|
963
|
+
skipResourceChecks: options.skipResourceChecks,
|
|
843
964
|
applicationInsights: config.applicationInsights ?? false
|
|
844
965
|
});
|
|
845
966
|
}
|
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';
|
|
@@ -30,6 +30,7 @@ declare function deploy(options: {
|
|
|
30
30
|
location?: string;
|
|
31
31
|
environment?: "dev" | "staging" | "prod";
|
|
32
32
|
skipInfrastructure?: boolean;
|
|
33
|
+
skipResourceChecks?: boolean;
|
|
33
34
|
}): Promise<void>;
|
|
34
35
|
|
|
35
36
|
export { build, deploy, init };
|
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';
|
|
@@ -30,6 +30,7 @@ declare function deploy(options: {
|
|
|
30
30
|
location?: string;
|
|
31
31
|
environment?: "dev" | "staging" | "prod";
|
|
32
32
|
skipInfrastructure?: boolean;
|
|
33
|
+
skipResourceChecks?: boolean;
|
|
33
34
|
}): Promise<void>;
|
|
34
35
|
|
|
35
36
|
export { build, deploy, init };
|
|
@@ -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 };
|