opennextjs-azure 0.1.4 → 0.2.0
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 +2 -1
- package/dist/adapters/converters/azure-http.d.mts +3 -1
- package/dist/adapters/converters/azure-http.d.ts +3 -1
- package/dist/adapters/converters/azure-http.js +24 -6
- package/dist/adapters/converters/azure-queue-revalidate.d.mts +26 -0
- package/dist/adapters/converters/azure-queue-revalidate.d.ts +26 -0
- package/dist/adapters/converters/azure-queue-revalidate.js +30 -0
- package/dist/adapters/wrappers/azure-functions.d.mts +8 -1
- package/dist/adapters/wrappers/azure-functions.d.ts +8 -1
- package/dist/adapters/wrappers/azure-functions.js +82 -12
- package/dist/adapters/wrappers/azure-image-optimization.js +53 -24
- package/dist/adapters/wrappers/azure-queue-revalidate.d.mts +9 -0
- package/dist/adapters/wrappers/azure-queue-revalidate.d.ts +9 -0
- package/dist/adapters/wrappers/azure-queue-revalidate.js +17 -0
- package/dist/cli/index.js +15 -5
- package/dist/config/index.js +9 -0
- package/dist/deploy.js +279 -27
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/infrastructure/main.bicep +13 -6
- package/dist/overrides/incrementalCache/azure-blob.d.mts +17 -1
- package/dist/overrides/incrementalCache/azure-blob.d.ts +17 -1
- package/dist/overrides/incrementalCache/azure-blob.js +20 -11
- package/dist/overrides/tagCache/azure-table.d.mts +24 -1
- package/dist/overrides/tagCache/azure-table.d.ts +24 -1
- package/dist/overrides/tagCache/azure-table.js +84 -27
- package/infrastructure/main.bicep +13 -6
- package/package.json +99 -90
- package/dist/adapters/image-optimization.d.mts +0 -20
- package/dist/adapters/image-optimization.d.ts +0 -20
- package/dist/adapters/image-optimization.js +0 -11
- package/dist/overrides/imageOptimization/azure-cached.d.mts +0 -6
- package/dist/overrides/imageOptimization/azure-cached.d.ts +0 -6
- package/dist/overrides/imageOptimization/azure-cached.js +0 -115
package/dist/deploy.js
CHANGED
|
@@ -5,6 +5,9 @@ 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
7
|
import fs$1, { existsSync } from 'node:fs';
|
|
8
|
+
import { BlobServiceClient } from '@azure/storage-blob';
|
|
9
|
+
import { TableClient } from '@azure/data-tables';
|
|
10
|
+
import { encodeTableKey } from './overrides/tagCache/azure-table.js';
|
|
8
11
|
import readline from 'node:readline';
|
|
9
12
|
|
|
10
13
|
const execAsync$3 = promisify(exec);
|
|
@@ -227,6 +230,35 @@ function redX() {
|
|
|
227
230
|
}
|
|
228
231
|
|
|
229
232
|
const execAsync$2 = promisify(exec);
|
|
233
|
+
async function readInstalledVersion(dep) {
|
|
234
|
+
try {
|
|
235
|
+
const pkgPath = path.join(process.cwd(), "node_modules", dep, "package.json");
|
|
236
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
237
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function pruneWrongPlatformBinaries(nodeModulesPath) {
|
|
243
|
+
const nextScope = path.join(nodeModulesPath, "@next");
|
|
244
|
+
try {
|
|
245
|
+
for (const entry of await fs.readdir(nextScope)) {
|
|
246
|
+
if (entry.startsWith("swc-")) {
|
|
247
|
+
await fs.rm(path.join(nextScope, entry), { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
const imgScope = path.join(nodeModulesPath, "@img");
|
|
253
|
+
try {
|
|
254
|
+
for (const entry of await fs.readdir(imgScope)) {
|
|
255
|
+
if (!entry.includes("linux-x64") && !entry.includes("wasm")) {
|
|
256
|
+
await fs.rm(path.join(imgScope, entry), { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
}
|
|
230
262
|
async function prepareFunctions() {
|
|
231
263
|
const functionsDir = path.join(process.cwd(), ".open-next/server-functions/default");
|
|
232
264
|
try {
|
|
@@ -251,7 +283,14 @@ async function prepareFunctions() {
|
|
|
251
283
|
},
|
|
252
284
|
extensions: {
|
|
253
285
|
http: {
|
|
254
|
-
routePrefix: ""
|
|
286
|
+
routePrefix: "",
|
|
287
|
+
// Bound per-instance concurrency and queueing so bursts
|
|
288
|
+
// trigger scale-out (the Consumption defaults queue ~200
|
|
289
|
+
// requests on a single 1-core SSR worker) while keeping
|
|
290
|
+
// enough queue to ride out the scale-out delay without
|
|
291
|
+
// shedding 429s.
|
|
292
|
+
maxConcurrentRequests: 16,
|
|
293
|
+
maxOutstandingRequests: 100
|
|
255
294
|
}
|
|
256
295
|
}
|
|
257
296
|
};
|
|
@@ -341,24 +380,72 @@ async function prepareFunctions() {
|
|
|
341
380
|
console.log(` ${greenCheck()} Image optimization function added`);
|
|
342
381
|
} catch {
|
|
343
382
|
}
|
|
383
|
+
const revalidationDir = path.join(process.cwd(), ".open-next/revalidation-function");
|
|
384
|
+
try {
|
|
385
|
+
await fs.access(path.join(revalidationDir, "index.mjs"));
|
|
386
|
+
console.log(" Adding revalidation queue consumer...");
|
|
387
|
+
const revalidateFunctionDir = path.join(functionsDir, "revalidate");
|
|
388
|
+
await fs.mkdir(revalidateFunctionDir, { recursive: true });
|
|
389
|
+
const revalidateFunctionJson = {
|
|
390
|
+
bindings: [
|
|
391
|
+
{
|
|
392
|
+
type: "queueTrigger",
|
|
393
|
+
direction: "in",
|
|
394
|
+
name: "queueItem",
|
|
395
|
+
// Binding expression: resolves the AZURE_QUEUE_NAME app
|
|
396
|
+
// setting (set by the bicep template), so the consumer
|
|
397
|
+
// follows the same queue the producer writes to.
|
|
398
|
+
queueName: "%AZURE_QUEUE_NAME%",
|
|
399
|
+
connection: "AZURE_STORAGE_CONNECTION_STRING"
|
|
400
|
+
}
|
|
401
|
+
],
|
|
402
|
+
scriptFile: "../index-revalidate.mjs",
|
|
403
|
+
entryPoint: "handler"
|
|
404
|
+
};
|
|
405
|
+
await fs.writeFile(
|
|
406
|
+
path.join(revalidateFunctionDir, "function.json"),
|
|
407
|
+
JSON.stringify(revalidateFunctionJson, null, 2)
|
|
408
|
+
);
|
|
409
|
+
await fs.copyFile(path.join(revalidationDir, "index.mjs"), path.join(functionsDir, "index-revalidate.mjs"));
|
|
410
|
+
await fs.copyFile(
|
|
411
|
+
path.join(revalidationDir, "prerender-manifest.json"),
|
|
412
|
+
path.join(functionsDir, "prerender-manifest.json")
|
|
413
|
+
);
|
|
414
|
+
console.log(` ${greenCheck()} Revalidation queue consumer added`);
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
344
417
|
console.log(` ${greenCheck()} Azure Functions metadata created`);
|
|
345
418
|
console.log("Installing minimal runtime dependencies...");
|
|
346
419
|
try {
|
|
347
420
|
const originalPackageJson = JSON.parse(await fs.readFile(path.join(functionsDir, "package.json"), "utf-8"));
|
|
421
|
+
const runtimeDependencies = {};
|
|
422
|
+
for (const [dep, spec] of Object.entries(
|
|
423
|
+
originalPackageJson.dependencies ?? {}
|
|
424
|
+
)) {
|
|
425
|
+
if (/^(file:|link:|workspace:|portal:|catalog:)/.test(spec)) {
|
|
426
|
+
const installedVersion = await readInstalledVersion(dep);
|
|
427
|
+
if (installedVersion && !spec.startsWith("file:") && !spec.startsWith("link:")) {
|
|
428
|
+
runtimeDependencies[dep] = installedVersion;
|
|
429
|
+
} else if (/^(workspace:|catalog:)/.test(spec)) {
|
|
430
|
+
console.warn(` Skipping "${dep}" (${spec}): not resolvable outside the workspace`);
|
|
431
|
+
}
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
runtimeDependencies[dep] = spec;
|
|
435
|
+
}
|
|
436
|
+
runtimeDependencies.next ||= "latest";
|
|
437
|
+
runtimeDependencies.react ||= "latest";
|
|
438
|
+
runtimeDependencies["react-dom"] ||= "latest";
|
|
348
439
|
const minimalPackageJson = {
|
|
349
440
|
name: originalPackageJson.name || "nextjs-app",
|
|
350
441
|
version: originalPackageJson.version || "1.0.0",
|
|
351
442
|
private: true,
|
|
352
|
-
dependencies:
|
|
353
|
-
next: originalPackageJson.dependencies?.next || "latest",
|
|
354
|
-
react: originalPackageJson.dependencies?.react || "latest",
|
|
355
|
-
"react-dom": originalPackageJson.dependencies?.["react-dom"] || "latest"
|
|
356
|
-
}
|
|
443
|
+
dependencies: runtimeDependencies
|
|
357
444
|
};
|
|
358
445
|
await fs.writeFile(path.join(functionsDir, "package.json"), JSON.stringify(minimalPackageJson, null, 2));
|
|
359
446
|
const nodeModulesPath = path.join(functionsDir, "node_modules");
|
|
360
447
|
await fs.rm(nodeModulesPath, { recursive: true, force: true });
|
|
361
|
-
await execAsync$2("npm install --
|
|
448
|
+
await execAsync$2("npm install --omit=dev --no-package-lock --loglevel=error --os=linux --cpu=x64 --libc=glibc", {
|
|
362
449
|
cwd: functionsDir
|
|
363
450
|
});
|
|
364
451
|
console.log(` ${greenCheck()} Runtime dependencies installed`);
|
|
@@ -371,12 +458,20 @@ async function prepareFunctions() {
|
|
|
371
458
|
await fs.access(imageOptDir2);
|
|
372
459
|
console.log("Installing Sharp with Linux x64 binaries for image optimization...");
|
|
373
460
|
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",
|
|
461
|
+
"npm install --force --no-package-lock --os=linux --cpu=x64 --libc=glibc sharp@0.33.5 @img/sharp-linux-x64@0.33.5 @img/sharp-libvips-linux-x64@1.0.4",
|
|
375
462
|
{ cwd: functionsDir }
|
|
376
463
|
);
|
|
377
464
|
console.log(` ${greenCheck()} Sharp with Linux x64 binaries installed`);
|
|
378
465
|
} catch (error) {
|
|
379
466
|
}
|
|
467
|
+
await pruneWrongPlatformBinaries(path.join(functionsDir, "node_modules"));
|
|
468
|
+
await fs.rm(path.join(functionsDir, "package-lock.json"), { force: true });
|
|
469
|
+
try {
|
|
470
|
+
const assetsDir = path.join(process.cwd(), ".open-next/assets");
|
|
471
|
+
const rootAssets = (await fs.readdir(assetsDir, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name);
|
|
472
|
+
await fs.writeFile(path.join(functionsDir, "static-assets.json"), JSON.stringify(rootAssets));
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
380
475
|
}
|
|
381
476
|
|
|
382
477
|
async function build(configPath) {
|
|
@@ -391,11 +486,12 @@ async function build(configPath) {
|
|
|
391
486
|
const { createRequire } = await import('node:module');
|
|
392
487
|
const require = createRequire(import.meta.url);
|
|
393
488
|
const packagePath = path.dirname(require.resolve("opennextjs-azure/package.json"));
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
const
|
|
489
|
+
const distPath = (rel) => path.join(packagePath, rel).split(path.sep).join("/");
|
|
490
|
+
const wrapperPath = distPath("dist/adapters/wrappers/azure-functions.js");
|
|
491
|
+
const converterPath = distPath("dist/adapters/converters/azure-http.js");
|
|
492
|
+
const incrementalCachePath = distPath("dist/overrides/incrementalCache/azure-blob.js");
|
|
493
|
+
const tagCachePath = distPath("dist/overrides/tagCache/azure-table.js");
|
|
494
|
+
const queuePath = distPath("dist/overrides/queue/azure-queue.js");
|
|
399
495
|
tempConfigPath = path.join(baseDir, "open-next.config.ts");
|
|
400
496
|
const configContent = `// @ts-nocheck
|
|
401
497
|
export default {
|
|
@@ -413,6 +509,12 @@ export default {
|
|
|
413
509
|
middleware: {
|
|
414
510
|
external: false,
|
|
415
511
|
},
|
|
512
|
+
revalidate: {
|
|
513
|
+
override: {
|
|
514
|
+
wrapper: () => import("${distPath("dist/adapters/wrappers/azure-queue-revalidate.js")}").then(m => m.default),
|
|
515
|
+
converter: () => import("${distPath("dist/adapters/converters/azure-queue-revalidate.js")}").then(m => m.default),
|
|
516
|
+
},
|
|
517
|
+
},
|
|
416
518
|
buildOutputPath: ".",
|
|
417
519
|
appPath: ".",
|
|
418
520
|
};
|
|
@@ -446,6 +548,81 @@ export default {
|
|
|
446
548
|
}
|
|
447
549
|
}
|
|
448
550
|
|
|
551
|
+
async function seedCacheAssets(connectionString, options = {}) {
|
|
552
|
+
const openNextDir = options.openNextDir ?? path.join(process.cwd(), ".open-next");
|
|
553
|
+
const containerName = options.containerName ?? process.env.AZURE_STORAGE_CONTAINER_NAME ?? "nextjs-cache";
|
|
554
|
+
const tableName = options.tableName ?? process.env.AZURE_TABLE_NAME ?? "nextjstags";
|
|
555
|
+
await Promise.all([
|
|
556
|
+
seedIncrementalCache(connectionString, path.join(openNextDir, "cache"), containerName),
|
|
557
|
+
seedTagTable(connectionString, path.join(openNextDir, "dynamodb-provider/dynamodb-cache.json"), tableName)
|
|
558
|
+
]);
|
|
559
|
+
}
|
|
560
|
+
async function runPool(items, limit, fn) {
|
|
561
|
+
let index = 0;
|
|
562
|
+
let done = 0;
|
|
563
|
+
await Promise.all(
|
|
564
|
+
Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
565
|
+
for (let i = index++; i < items.length; i = index++) {
|
|
566
|
+
await fn(items[i]);
|
|
567
|
+
done++;
|
|
568
|
+
}
|
|
569
|
+
})
|
|
570
|
+
);
|
|
571
|
+
return done;
|
|
572
|
+
}
|
|
573
|
+
async function seedIncrementalCache(connectionString, cacheDir, containerName = "nextjs-cache") {
|
|
574
|
+
let files;
|
|
575
|
+
try {
|
|
576
|
+
files = (await fs.readdir(cacheDir, { recursive: true, withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => path.join(entry.parentPath ?? entry.path, entry.name));
|
|
577
|
+
} catch {
|
|
578
|
+
return 0;
|
|
579
|
+
}
|
|
580
|
+
const container = BlobServiceClient.fromConnectionString(connectionString).getContainerClient(containerName);
|
|
581
|
+
await container.createIfNotExists();
|
|
582
|
+
const keyPrefix = process.env.AZURE_CACHE_KEY_PREFIX ? `${process.env.AZURE_CACHE_KEY_PREFIX}/` : "";
|
|
583
|
+
return runPool(files, 8, async (file) => {
|
|
584
|
+
const blobName = keyPrefix + path.relative(cacheDir, file).split(path.sep).join("/");
|
|
585
|
+
await container.getBlockBlobClient(blobName).uploadFile(file, {
|
|
586
|
+
blobHTTPHeaders: { blobContentType: "application/json" }
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
async function seedTagTable(connectionString, seedFile, tableName = "nextjstags") {
|
|
591
|
+
let entries;
|
|
592
|
+
try {
|
|
593
|
+
entries = JSON.parse(await fs.readFile(seedFile, "utf-8"));
|
|
594
|
+
} catch {
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
597
|
+
const table = TableClient.fromConnectionString(connectionString, tableName, {
|
|
598
|
+
allowInsecureConnection: connectionString.includes("http://")
|
|
599
|
+
});
|
|
600
|
+
await table.createTable().catch((error) => {
|
|
601
|
+
if (error.statusCode !== 409)
|
|
602
|
+
throw error;
|
|
603
|
+
});
|
|
604
|
+
return runPool(entries, 8, async (entry) => {
|
|
605
|
+
await Promise.all([
|
|
606
|
+
table.upsertEntity(
|
|
607
|
+
{
|
|
608
|
+
partitionKey: encodeTableKey(entry.tag.S),
|
|
609
|
+
rowKey: encodeTableKey(entry.path.S),
|
|
610
|
+
revalidatedAt: Number(entry.revalidatedAt.N)
|
|
611
|
+
},
|
|
612
|
+
"Merge"
|
|
613
|
+
),
|
|
614
|
+
table.upsertEntity(
|
|
615
|
+
{
|
|
616
|
+
partitionKey: encodeTableKey(`path#${entry.path.S}`),
|
|
617
|
+
rowKey: encodeTableKey(entry.tag.S),
|
|
618
|
+
revalidatedAt: Number(entry.revalidatedAt.N)
|
|
619
|
+
},
|
|
620
|
+
"Merge"
|
|
621
|
+
)
|
|
622
|
+
]);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
449
626
|
const execAsync$1 = promisify(exec);
|
|
450
627
|
const colors = {
|
|
451
628
|
green: "\x1B[32m",
|
|
@@ -479,12 +656,14 @@ async function deploy$1(options) {
|
|
|
479
656
|
await checkExistingInfrastructure(appName, resourceGroup, environment);
|
|
480
657
|
}
|
|
481
658
|
let deploymentOutputs;
|
|
659
|
+
const expectedFunctionAppName = `${appName}-func-${environment}`;
|
|
482
660
|
if (!skipInfrastructure) {
|
|
483
661
|
await syncBicepTemplate();
|
|
484
662
|
console.log("Provisioning Azure infrastructure...");
|
|
485
663
|
console.log(` Resource Group: ${resourceGroup}`);
|
|
486
664
|
console.log(` Location: ${location}`);
|
|
487
665
|
console.log(` Environment: ${environment}`);
|
|
666
|
+
const runFromPackage = await readRunFromPackage(resourceGroup, expectedFunctionAppName);
|
|
488
667
|
deploymentOutputs = await provisionInfrastructure({
|
|
489
668
|
appName,
|
|
490
669
|
resourceGroup,
|
|
@@ -492,13 +671,22 @@ async function deploy$1(options) {
|
|
|
492
671
|
environment,
|
|
493
672
|
applicationInsights: options.applicationInsights ?? false
|
|
494
673
|
});
|
|
674
|
+
if (runFromPackage?.startsWith("http")) {
|
|
675
|
+
await execAsync$1(
|
|
676
|
+
`az functionapp config appsettings set --resource-group ${resourceGroup} --name ${expectedFunctionAppName} --settings "WEBSITE_RUN_FROM_PACKAGE=${runFromPackage}" --output none`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
495
679
|
console.log(` ${greenCheck()} Infrastructure ready`);
|
|
496
680
|
} else {
|
|
497
681
|
console.log("Skipping infrastructure provisioning");
|
|
498
682
|
}
|
|
683
|
+
const storageAccountName = deploymentOutputs?.storageAccount || await getStorageAccountName(resourceGroup);
|
|
499
684
|
console.log("Uploading static assets...");
|
|
500
|
-
await uploadStaticAssets(
|
|
685
|
+
await uploadStaticAssets(storageAccountName);
|
|
501
686
|
console.log(` ${greenCheck()} Assets uploaded`);
|
|
687
|
+
console.log("Seeding ISR cache and tag table...");
|
|
688
|
+
await seedRuntimeCaches(resourceGroup, storageAccountName);
|
|
689
|
+
console.log(` ${greenCheck()} Cache seeded`);
|
|
502
690
|
console.log("Deploying Function App...");
|
|
503
691
|
const functionAppName = deploymentOutputs?.functionApp || `${appName}-func-${environment}`;
|
|
504
692
|
await deployFunctionApp(functionAppName, resourceGroup);
|
|
@@ -571,7 +759,7 @@ async function checkQuotaAvailability(location, environment) {
|
|
|
571
759
|
const ep1Limit = ep1Quota?.properties?.limit?.value || 0;
|
|
572
760
|
const skuMap = {
|
|
573
761
|
dev: { name: "Y1 (Consumption)", quota: y1Limit, type: "Dynamic" },
|
|
574
|
-
staging: { name: "
|
|
762
|
+
staging: { name: "Y1 (Consumption)", quota: y1Limit, type: "Dynamic" },
|
|
575
763
|
prod: { name: "EP1 (Elastic Premium)", quota: ep1Limit, type: "ElasticPremium" }
|
|
576
764
|
};
|
|
577
765
|
const requiredSku = skuMap[environment];
|
|
@@ -800,7 +988,7 @@ async function provisionInfrastructure(options) {
|
|
|
800
988
|
const bicepPath = path.join(process.cwd(), "infrastructure/main.bicep");
|
|
801
989
|
const enableAppInsights = applicationInsights ? "true" : "false";
|
|
802
990
|
const { stdout } = await execAsync$1(
|
|
803
|
-
`az deployment group create --resource-group ${resourceGroup} --template-file ${bicepPath} --parameters appName=${appName} environment=${environment} enableApplicationInsights=${enableAppInsights} --query 'properties.outputs.deploymentInfo.value' --output json`
|
|
991
|
+
`az deployment group create --resource-group ${resourceGroup} --template-file "${bicepPath}" --parameters appName=${appName} environment=${environment} enableApplicationInsights=${enableAppInsights} --query 'properties.outputs.deploymentInfo.value' --output json`
|
|
804
992
|
);
|
|
805
993
|
return JSON.parse(stdout);
|
|
806
994
|
}
|
|
@@ -818,31 +1006,56 @@ async function patchCSSForBlobStorage(assetsPath) {
|
|
|
818
1006
|
await fs.writeFile(filePath, content, "utf-8");
|
|
819
1007
|
}
|
|
820
1008
|
}
|
|
821
|
-
async function
|
|
822
|
-
const assetsPath = path.join(process.cwd(), ".open-next/assets");
|
|
823
|
-
await patchCSSForBlobStorage(assetsPath);
|
|
1009
|
+
async function getStorageAccountName(resourceGroup) {
|
|
824
1010
|
const { stdout } = await execAsync$1(
|
|
825
1011
|
`az storage account list --resource-group ${resourceGroup} --query "[0].name" -o tsv`
|
|
826
1012
|
);
|
|
827
|
-
const
|
|
1013
|
+
const name = stdout.trim();
|
|
1014
|
+
if (!name) {
|
|
1015
|
+
throw new Error(`No storage account found in resource group "${resourceGroup}"`);
|
|
1016
|
+
}
|
|
1017
|
+
return name;
|
|
1018
|
+
}
|
|
1019
|
+
async function readRunFromPackage(resourceGroup, functionAppName) {
|
|
1020
|
+
try {
|
|
1021
|
+
const { stdout } = await execAsync$1(
|
|
1022
|
+
`az functionapp config appsettings list --resource-group ${resourceGroup} --name ${functionAppName} --query "[?name=='WEBSITE_RUN_FROM_PACKAGE'].value | [0]" -o tsv`
|
|
1023
|
+
);
|
|
1024
|
+
return stdout.trim() || null;
|
|
1025
|
+
} catch {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
async function seedRuntimeCaches(resourceGroup, storageAccountName) {
|
|
1030
|
+
const { stdout: connectionString } = await execAsync$1(
|
|
1031
|
+
`az storage account show-connection-string --resource-group ${resourceGroup} --name ${storageAccountName} --query connectionString -o tsv`
|
|
1032
|
+
);
|
|
1033
|
+
await seedCacheAssets(connectionString.trim());
|
|
1034
|
+
}
|
|
1035
|
+
async function uploadStaticAssets(storageAccountName) {
|
|
1036
|
+
const assetsPath = path.join(process.cwd(), ".open-next/assets");
|
|
1037
|
+
await patchCSSForBlobStorage(assetsPath);
|
|
828
1038
|
await execAsync$1(
|
|
829
|
-
`az storage blob upload-batch --account-name ${storageAccountName} --destination assets --source ${assetsPath} --content-cache-control "public, max-age=0, must-revalidate" --overwrite
|
|
1039
|
+
`az storage blob upload-batch --account-name ${storageAccountName} --destination assets --source "${assetsPath}" --content-cache-control "public, max-age=0, must-revalidate" --overwrite`,
|
|
1040
|
+
{ maxBuffer: 100 * 1024 * 1024 }
|
|
830
1041
|
);
|
|
831
1042
|
const nextStaticPath = path.join(assetsPath, "_next/static");
|
|
832
1043
|
if (existsSync(nextStaticPath)) {
|
|
833
1044
|
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
|
|
1045
|
+
`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`,
|
|
1046
|
+
{ maxBuffer: 100 * 1024 * 1024 }
|
|
835
1047
|
);
|
|
836
1048
|
}
|
|
837
1049
|
}
|
|
838
1050
|
async function deployFunctionApp(functionAppName, resourceGroup) {
|
|
839
1051
|
const functionsPath = path.join(process.cwd(), ".open-next/server-functions/default");
|
|
840
1052
|
const zipPath = path.join(process.cwd(), ".open-next/function-app.zip");
|
|
841
|
-
await
|
|
1053
|
+
await fs.rm(zipPath, { force: true });
|
|
1054
|
+
await execAsync$1(`cd "${functionsPath}" && zip -r "${zipPath}" . -q`, {
|
|
842
1055
|
maxBuffer: 100 * 1024 * 1024
|
|
843
1056
|
});
|
|
844
1057
|
await execAsync$1(
|
|
845
|
-
`az functionapp deployment source config-zip --resource-group ${resourceGroup} --name ${functionAppName} --src ${zipPath}`,
|
|
1058
|
+
`az functionapp deployment source config-zip --resource-group ${resourceGroup} --name ${functionAppName} --src "${zipPath}"`,
|
|
846
1059
|
{
|
|
847
1060
|
maxBuffer: 100 * 1024 * 1024
|
|
848
1061
|
}
|
|
@@ -850,6 +1063,41 @@ async function deployFunctionApp(functionAppName, resourceGroup) {
|
|
|
850
1063
|
await fs.unlink(zipPath);
|
|
851
1064
|
}
|
|
852
1065
|
|
|
1066
|
+
const RULES = {
|
|
1067
|
+
appName: {
|
|
1068
|
+
pattern: /^[a-zA-Z0-9][a-zA-Z0-9-]{0,58}[a-zA-Z0-9]$/,
|
|
1069
|
+
hint: "2-60 alphanumeric characters or hyphens, starting and ending with an alphanumeric"
|
|
1070
|
+
},
|
|
1071
|
+
resourceGroup: {
|
|
1072
|
+
pattern: /^[a-zA-Z0-9_()\-.]{1,90}$/,
|
|
1073
|
+
hint: "1-90 alphanumerics, underscores, parentheses, hyphens, or periods"
|
|
1074
|
+
},
|
|
1075
|
+
location: {
|
|
1076
|
+
pattern: /^[a-z0-9]{3,30}$/,
|
|
1077
|
+
hint: "a lowercase Azure region name like eastus or westeurope"
|
|
1078
|
+
},
|
|
1079
|
+
environment: {
|
|
1080
|
+
pattern: /^(dev|staging|prod)$/,
|
|
1081
|
+
hint: "one of: dev, staging, prod"
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
function validateAzureName(kind, value) {
|
|
1085
|
+
if (value === void 0)
|
|
1086
|
+
return;
|
|
1087
|
+
const rule = RULES[kind];
|
|
1088
|
+
if (!rule.pattern.test(value)) {
|
|
1089
|
+
console.error(`\u274C Invalid ${kind}: ${JSON.stringify(value)}`);
|
|
1090
|
+
console.error(` Expected ${rule.hint}.`);
|
|
1091
|
+
process.exit(1);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
function validateAzureNames(values) {
|
|
1095
|
+
validateAzureName("appName", values.appName);
|
|
1096
|
+
validateAzureName("resourceGroup", values.resourceGroup);
|
|
1097
|
+
validateAzureName("location", values.location);
|
|
1098
|
+
validateAzureName("environment", values.environment);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
853
1101
|
const execAsync = promisify(exec);
|
|
854
1102
|
async function promptForInput(question) {
|
|
855
1103
|
const rl = readline.createInterface({
|
|
@@ -947,6 +1195,7 @@ async function deploy(options) {
|
|
|
947
1195
|
if (!resourceGroup) {
|
|
948
1196
|
const result = await selectResourceGroup();
|
|
949
1197
|
resourceGroup = result.name;
|
|
1198
|
+
validateAzureNames({ resourceGroup });
|
|
950
1199
|
if (result.isNew) {
|
|
951
1200
|
resourceGroupLocation = result.location;
|
|
952
1201
|
} else {
|
|
@@ -954,10 +1203,13 @@ async function deploy(options) {
|
|
|
954
1203
|
}
|
|
955
1204
|
}
|
|
956
1205
|
const environment = options.environment || config.environment || await selectEnvironment();
|
|
1206
|
+
const appName = options.appName || config.appName || path.basename(cwd);
|
|
1207
|
+
const location = resourceGroupLocation || options.location || config.location || "eastus";
|
|
1208
|
+
validateAzureNames({ appName, resourceGroup, location, environment });
|
|
957
1209
|
await deploy$1({
|
|
958
|
-
appName
|
|
1210
|
+
appName,
|
|
959
1211
|
resourceGroup,
|
|
960
|
-
location
|
|
1212
|
+
location,
|
|
961
1213
|
environment,
|
|
962
1214
|
skipInfrastructure: options.skipInfrastructure,
|
|
963
1215
|
skipResourceChecks: options.skipResourceChecks,
|
|
@@ -976,4 +1228,4 @@ async function getResourceGroupLocation(resourceGroup) {
|
|
|
976
1228
|
}
|
|
977
1229
|
}
|
|
978
1230
|
|
|
979
|
-
export { build as b, deploy as d, greenCheck as g, init as i, promptForInput as p, redX as r };
|
|
1231
|
+
export { build as b, deploy as d, greenCheck as g, init as i, promptForInput as p, redX as r, validateAzureNames as v };
|
package/dist/index.d.mts
CHANGED
|
@@ -7,6 +7,7 @@ export { default as azureHttpConverter } from './adapters/converters/azure-http.
|
|
|
7
7
|
import '@opennextjs/aws/types/open-next.js';
|
|
8
8
|
import '@opennextjs/aws/types/overrides.js';
|
|
9
9
|
import '@azure/functions';
|
|
10
|
+
import 'node:buffer';
|
|
10
11
|
|
|
11
12
|
interface ScaffoldOptions {
|
|
12
13
|
typescript?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { default as azureHttpConverter } from './adapters/converters/azure-http.
|
|
|
7
7
|
import '@opennextjs/aws/types/open-next.js';
|
|
8
8
|
import '@opennextjs/aws/types/overrides.js';
|
|
9
9
|
import '@azure/functions';
|
|
10
|
+
import 'node:buffer';
|
|
10
11
|
|
|
11
12
|
interface ScaffoldOptions {
|
|
12
13
|
typescript?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -12,9 +12,9 @@ import 'node:child_process';
|
|
|
12
12
|
import 'node:util';
|
|
13
13
|
import '@opennextjs/aws/build.js';
|
|
14
14
|
import 'node:fs';
|
|
15
|
-
import 'node:readline';
|
|
16
15
|
import '@azure/storage-blob';
|
|
17
16
|
import '@azure/data-tables';
|
|
17
|
+
import 'node:readline';
|
|
18
18
|
import '@azure/storage-queue';
|
|
19
19
|
import 'node:stream';
|
|
20
20
|
import 'node:buffer';
|
|
@@ -136,6 +136,9 @@ resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
|
|
|
136
136
|
kind: 'functionapp'
|
|
137
137
|
properties: {
|
|
138
138
|
reserved: true // Linux
|
|
139
|
+
// Elastic Premium defaults to a max of 1 worker when this is omitted,
|
|
140
|
+
// which pins the prod plan to a single instance. Y1 ignores it.
|
|
141
|
+
maximumElasticWorkerCount: environment == 'prod' ? 20 : 1
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
|
|
@@ -170,12 +173,10 @@ resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
|
|
|
170
173
|
value: 'node'
|
|
171
174
|
}
|
|
172
175
|
{
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
name: 'WEBSITE_RUN_FROM_PACKAGE'
|
|
178
|
-
value: '1'
|
|
176
|
+
// A second worker process buys resilience to GC stalls; SSR is
|
|
177
|
+
// single-threaded per worker. Memory-bounded on Y1 (1.5 GB).
|
|
178
|
+
name: 'FUNCTIONS_WORKER_PROCESS_COUNT'
|
|
179
|
+
value: '2'
|
|
179
180
|
}
|
|
180
181
|
{
|
|
181
182
|
name: 'AzureWebJobsDisableHomepage'
|
|
@@ -223,6 +224,12 @@ resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
|
|
|
223
224
|
|
|
224
225
|
ftpsState: 'Disabled'
|
|
225
226
|
minTlsVersion: '1.2'
|
|
227
|
+
// Always-ready SSR on Elastic Premium; both stay null on Y1.
|
|
228
|
+
// WEBSITE_RUN_FROM_PACKAGE is deliberately absent: '1' is unsupported
|
|
229
|
+
// on Linux Consumption, and the deploy step sets the blob-URL form
|
|
230
|
+
// that provisioning must not overwrite.
|
|
231
|
+
minimumElasticInstanceCount: environment == 'prod' ? 1 : null
|
|
232
|
+
preWarmedInstanceCount: environment == 'prod' ? 1 : null
|
|
226
233
|
cors: {
|
|
227
234
|
allowedOrigins: ['*']
|
|
228
235
|
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { IncrementalCache, CacheEntryType, WithLastModified, CacheValue } from '@opennextjs/aws/types/overrides.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Retry policy for blob calls on the request hot path. The SDK's default
|
|
5
|
+
* first retry waits 4s; fail fast instead and let cache layers treat
|
|
6
|
+
* errors as misses. Shared with the image-optimization wrapper.
|
|
7
|
+
*/
|
|
8
|
+
declare const BLOB_RETRY_OPTIONS: {
|
|
9
|
+
retryOptions: {
|
|
10
|
+
maxTries: number;
|
|
11
|
+
retryDelayInMs: number;
|
|
12
|
+
maxRetryDelayInMs: number;
|
|
13
|
+
tryTimeoutInMs: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
3
16
|
/**
|
|
4
17
|
* Azure Blob Storage implementation of IncrementalCache.
|
|
5
18
|
*
|
|
@@ -13,6 +26,9 @@ declare class AzureBlobIncrementalCache implements IncrementalCache {
|
|
|
13
26
|
/**
|
|
14
27
|
* Builds the blob key path, mimicking S3 structure:
|
|
15
28
|
* [prefix]/[__fetch]/[buildId]/[key].[extension]
|
|
29
|
+
*
|
|
30
|
+
* Keys must match the .open-next/cache layout the seeder uploads, so no
|
|
31
|
+
* container-name prefix and no leading slash.
|
|
16
32
|
*/
|
|
17
33
|
private buildBlobKey;
|
|
18
34
|
get<CacheType extends CacheEntryType = "cache">(key: string, cacheType?: CacheType): Promise<WithLastModified<CacheValue<CacheType>> | null>;
|
|
@@ -20,4 +36,4 @@ declare class AzureBlobIncrementalCache implements IncrementalCache {
|
|
|
20
36
|
delete(key: string): Promise<void>;
|
|
21
37
|
}
|
|
22
38
|
|
|
23
|
-
export { AzureBlobIncrementalCache as default };
|
|
39
|
+
export { BLOB_RETRY_OPTIONS, AzureBlobIncrementalCache as default };
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { IncrementalCache, CacheEntryType, WithLastModified, CacheValue } from '@opennextjs/aws/types/overrides.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Retry policy for blob calls on the request hot path. The SDK's default
|
|
5
|
+
* first retry waits 4s; fail fast instead and let cache layers treat
|
|
6
|
+
* errors as misses. Shared with the image-optimization wrapper.
|
|
7
|
+
*/
|
|
8
|
+
declare const BLOB_RETRY_OPTIONS: {
|
|
9
|
+
retryOptions: {
|
|
10
|
+
maxTries: number;
|
|
11
|
+
retryDelayInMs: number;
|
|
12
|
+
maxRetryDelayInMs: number;
|
|
13
|
+
tryTimeoutInMs: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
3
16
|
/**
|
|
4
17
|
* Azure Blob Storage implementation of IncrementalCache.
|
|
5
18
|
*
|
|
@@ -13,6 +26,9 @@ declare class AzureBlobIncrementalCache implements IncrementalCache {
|
|
|
13
26
|
/**
|
|
14
27
|
* Builds the blob key path, mimicking S3 structure:
|
|
15
28
|
* [prefix]/[__fetch]/[buildId]/[key].[extension]
|
|
29
|
+
*
|
|
30
|
+
* Keys must match the .open-next/cache layout the seeder uploads, so no
|
|
31
|
+
* container-name prefix and no leading slash.
|
|
16
32
|
*/
|
|
17
33
|
private buildBlobKey;
|
|
18
34
|
get<CacheType extends CacheEntryType = "cache">(key: string, cacheType?: CacheType): Promise<WithLastModified<CacheValue<CacheType>> | null>;
|
|
@@ -20,4 +36,4 @@ declare class AzureBlobIncrementalCache implements IncrementalCache {
|
|
|
20
36
|
delete(key: string): Promise<void>;
|
|
21
37
|
}
|
|
22
38
|
|
|
23
|
-
export { AzureBlobIncrementalCache as default };
|
|
39
|
+
export { BLOB_RETRY_OPTIONS, AzureBlobIncrementalCache as default };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { BlobServiceClient } from '@azure/storage-blob';
|
|
1
|
+
import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob';
|
|
2
2
|
import { getAzureConfig } from '../../config/index.js';
|
|
3
3
|
|
|
4
|
+
const BLOB_RETRY_OPTIONS = {
|
|
5
|
+
retryOptions: { maxTries: 3, retryDelayInMs: 300, maxRetryDelayInMs: 2e3, tryTimeoutInMs: 1e4 }
|
|
6
|
+
};
|
|
4
7
|
class AzureBlobIncrementalCache {
|
|
5
8
|
name = "azure-blob";
|
|
6
9
|
containerClient;
|
|
@@ -9,27 +12,33 @@ class AzureBlobIncrementalCache {
|
|
|
9
12
|
const connectionString = storage.connectionString;
|
|
10
13
|
const accountName = storage.accountName;
|
|
11
14
|
const accountKey = storage.accountKey;
|
|
15
|
+
const clientOptions = BLOB_RETRY_OPTIONS;
|
|
12
16
|
if (connectionString) {
|
|
13
|
-
const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
|
|
17
|
+
const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString, clientOptions);
|
|
14
18
|
this.containerClient = blobServiceClient.getContainerClient(storage.containerName || "nextjs-cache");
|
|
15
19
|
} else if (accountName && accountKey) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
const credential = new StorageSharedKeyCredential(accountName, accountKey);
|
|
21
|
+
const blobServiceClient = new BlobServiceClient(
|
|
22
|
+
`https://${accountName}.blob.core.windows.net`,
|
|
23
|
+
credential,
|
|
24
|
+
clientOptions
|
|
25
|
+
);
|
|
20
26
|
this.containerClient = blobServiceClient.getContainerClient(storage.containerName || "nextjs-cache");
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
/**
|
|
24
30
|
* Builds the blob key path, mimicking S3 structure:
|
|
25
31
|
* [prefix]/[__fetch]/[buildId]/[key].[extension]
|
|
32
|
+
*
|
|
33
|
+
* Keys must match the .open-next/cache layout the seeder uploads, so no
|
|
34
|
+
* container-name prefix and no leading slash.
|
|
26
35
|
*/
|
|
27
36
|
buildBlobKey(key, cacheType = "cache") {
|
|
28
|
-
const { storage } = getAzureConfig();
|
|
29
37
|
const { NEXT_BUILD_ID } = process.env;
|
|
30
|
-
const prefix =
|
|
38
|
+
const prefix = process.env.AZURE_CACHE_KEY_PREFIX || "";
|
|
31
39
|
const type = cacheType === "fetch" ? "__fetch" : "";
|
|
32
|
-
|
|
40
|
+
const cleanKey = key.replace(/^\/+/, "");
|
|
41
|
+
return [prefix, type, NEXT_BUILD_ID, cacheType === "fetch" ? cleanKey : `${cleanKey}.${cacheType}`].filter(Boolean).join("/");
|
|
33
42
|
}
|
|
34
43
|
async get(key, cacheType) {
|
|
35
44
|
try {
|
|
@@ -63,7 +72,7 @@ class AzureBlobIncrementalCache {
|
|
|
63
72
|
const blobKey = this.buildBlobKey(key, cacheType);
|
|
64
73
|
const blobClient = this.containerClient.getBlockBlobClient(blobKey);
|
|
65
74
|
const content = JSON.stringify(value);
|
|
66
|
-
await blobClient.upload(content, content
|
|
75
|
+
await blobClient.upload(content, Buffer.byteLength(content), {
|
|
67
76
|
blobHTTPHeaders: {
|
|
68
77
|
blobContentType: "application/json"
|
|
69
78
|
}
|
|
@@ -86,4 +95,4 @@ class AzureBlobIncrementalCache {
|
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
export { AzureBlobIncrementalCache as default };
|
|
98
|
+
export { BLOB_RETRY_OPTIONS, AzureBlobIncrementalCache as default };
|