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/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
- console.log(` ${greenCheck()} Azure Functions metadata created
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...\n");
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\n");
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("\nOutput: .open-next/");
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)\n");
344
- console.log("Next: opennextjs-azure deploy\n");
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\n");
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 = ["Microsoft.Web", "Microsoft.Storage", "Microsoft.Compute", "Microsoft.Quota"];
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...\n");
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("\nApplication:");
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("\nInfrastructure:");
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("\nConfiguration:");
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 create --name ${resourceGroup} --location ${location}`);
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.d619537c.mjs';
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.d619537c.js';
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,5 @@
1
+ import { ImageLoader } from '@opennextjs/aws/types/overrides.js';
2
+
3
+ declare const azureBlobImageLoader: ImageLoader;
4
+
5
+ export { azureBlobImageLoader as default };
@@ -0,0 +1,5 @@
1
+ import { ImageLoader } from '@opennextjs/aws/types/overrides.js';
2
+
3
+ declare const azureBlobImageLoader: ImageLoader;
4
+
5
+ export { azureBlobImageLoader as default };
@@ -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 };