opennextjs-azure 0.1.3 → 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 +301 -33
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -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 +13 -4
- 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",
|
|
@@ -459,27 +636,34 @@ async function deploy$1(options) {
|
|
|
459
636
|
resourceGroup = `${appName}-rg`,
|
|
460
637
|
location = "eastus",
|
|
461
638
|
environment = "dev",
|
|
462
|
-
skipInfrastructure = false
|
|
639
|
+
skipInfrastructure = false,
|
|
640
|
+
skipResourceChecks = false
|
|
463
641
|
} = options;
|
|
464
642
|
console.log(`Deploying ${appName} to Azure (${environment} environment)`);
|
|
465
643
|
try {
|
|
466
644
|
await checkAzureCLI();
|
|
467
645
|
await checkAzureLogin();
|
|
468
|
-
await checkAzureSubscriptionPermissions();
|
|
469
|
-
await checkLocation(location);
|
|
470
|
-
await checkRequiredProviders(options.applicationInsights);
|
|
471
|
-
await checkQuotaAvailability(location, environment);
|
|
472
646
|
await checkBuildOutput();
|
|
647
|
+
if (!skipResourceChecks) {
|
|
648
|
+
await checkAzureSubscriptionPermissions();
|
|
649
|
+
await checkLocation(location);
|
|
650
|
+
await checkRequiredProviders(options.applicationInsights);
|
|
651
|
+
await checkQuotaAvailability(location, environment);
|
|
652
|
+
} else {
|
|
653
|
+
console.log("Skipping resource checks (--skip-resource-checks)");
|
|
654
|
+
}
|
|
473
655
|
if (skipInfrastructure) {
|
|
474
656
|
await checkExistingInfrastructure(appName, resourceGroup, environment);
|
|
475
657
|
}
|
|
476
658
|
let deploymentOutputs;
|
|
659
|
+
const expectedFunctionAppName = `${appName}-func-${environment}`;
|
|
477
660
|
if (!skipInfrastructure) {
|
|
478
661
|
await syncBicepTemplate();
|
|
479
662
|
console.log("Provisioning Azure infrastructure...");
|
|
480
663
|
console.log(` Resource Group: ${resourceGroup}`);
|
|
481
664
|
console.log(` Location: ${location}`);
|
|
482
665
|
console.log(` Environment: ${environment}`);
|
|
666
|
+
const runFromPackage = await readRunFromPackage(resourceGroup, expectedFunctionAppName);
|
|
483
667
|
deploymentOutputs = await provisionInfrastructure({
|
|
484
668
|
appName,
|
|
485
669
|
resourceGroup,
|
|
@@ -487,13 +671,22 @@ async function deploy$1(options) {
|
|
|
487
671
|
environment,
|
|
488
672
|
applicationInsights: options.applicationInsights ?? false
|
|
489
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
|
+
}
|
|
490
679
|
console.log(` ${greenCheck()} Infrastructure ready`);
|
|
491
680
|
} else {
|
|
492
681
|
console.log("Skipping infrastructure provisioning");
|
|
493
682
|
}
|
|
683
|
+
const storageAccountName = deploymentOutputs?.storageAccount || await getStorageAccountName(resourceGroup);
|
|
494
684
|
console.log("Uploading static assets...");
|
|
495
|
-
await uploadStaticAssets(
|
|
685
|
+
await uploadStaticAssets(storageAccountName);
|
|
496
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`);
|
|
497
690
|
console.log("Deploying Function App...");
|
|
498
691
|
const functionAppName = deploymentOutputs?.functionApp || `${appName}-func-${environment}`;
|
|
499
692
|
await deployFunctionApp(functionAppName, resourceGroup);
|
|
@@ -566,7 +759,7 @@ async function checkQuotaAvailability(location, environment) {
|
|
|
566
759
|
const ep1Limit = ep1Quota?.properties?.limit?.value || 0;
|
|
567
760
|
const skuMap = {
|
|
568
761
|
dev: { name: "Y1 (Consumption)", quota: y1Limit, type: "Dynamic" },
|
|
569
|
-
staging: { name: "
|
|
762
|
+
staging: { name: "Y1 (Consumption)", quota: y1Limit, type: "Dynamic" },
|
|
570
763
|
prod: { name: "EP1 (Elastic Premium)", quota: ep1Limit, type: "ElasticPremium" }
|
|
571
764
|
};
|
|
572
765
|
const requiredSku = skuMap[environment];
|
|
@@ -781,11 +974,21 @@ async function syncBicepTemplate() {
|
|
|
781
974
|
}
|
|
782
975
|
async function provisionInfrastructure(options) {
|
|
783
976
|
const { appName, resourceGroup, location, environment, applicationInsights } = options;
|
|
784
|
-
await execAsync$1(`az group
|
|
977
|
+
const { stdout: rgExists } = await execAsync$1(`az group exists --name ${resourceGroup}`);
|
|
978
|
+
if (rgExists.trim() === "true") {
|
|
979
|
+
const { stdout: rgLocation } = await execAsync$1(`az group show --name ${resourceGroup} --query location -o tsv`);
|
|
980
|
+
if (rgLocation.trim() !== location) {
|
|
981
|
+
console.warn(
|
|
982
|
+
` ${colors.yellow}Warning:${colors.reset} Resource group "${resourceGroup}" exists in ${rgLocation.trim()}, but ${location} was specified`
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
} else {
|
|
986
|
+
await execAsync$1(`az group create --name ${resourceGroup} --location ${location}`);
|
|
987
|
+
}
|
|
785
988
|
const bicepPath = path.join(process.cwd(), "infrastructure/main.bicep");
|
|
786
989
|
const enableAppInsights = applicationInsights ? "true" : "false";
|
|
787
990
|
const { stdout } = await execAsync$1(
|
|
788
|
-
`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`
|
|
789
992
|
);
|
|
790
993
|
return JSON.parse(stdout);
|
|
791
994
|
}
|
|
@@ -803,31 +1006,56 @@ async function patchCSSForBlobStorage(assetsPath) {
|
|
|
803
1006
|
await fs.writeFile(filePath, content, "utf-8");
|
|
804
1007
|
}
|
|
805
1008
|
}
|
|
806
|
-
async function
|
|
807
|
-
const assetsPath = path.join(process.cwd(), ".open-next/assets");
|
|
808
|
-
await patchCSSForBlobStorage(assetsPath);
|
|
1009
|
+
async function getStorageAccountName(resourceGroup) {
|
|
809
1010
|
const { stdout } = await execAsync$1(
|
|
810
1011
|
`az storage account list --resource-group ${resourceGroup} --query "[0].name" -o tsv`
|
|
811
1012
|
);
|
|
812
|
-
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);
|
|
813
1038
|
await execAsync$1(
|
|
814
|
-
`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 }
|
|
815
1041
|
);
|
|
816
1042
|
const nextStaticPath = path.join(assetsPath, "_next/static");
|
|
817
1043
|
if (existsSync(nextStaticPath)) {
|
|
818
1044
|
await execAsync$1(
|
|
819
|
-
`az storage blob upload-batch --account-name ${storageAccountName} --destination assets --source ${nextStaticPath} --destination-path _next/static --content-cache-control "public, max-age=31536000, immutable" --overwrite
|
|
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 }
|
|
820
1047
|
);
|
|
821
1048
|
}
|
|
822
1049
|
}
|
|
823
1050
|
async function deployFunctionApp(functionAppName, resourceGroup) {
|
|
824
1051
|
const functionsPath = path.join(process.cwd(), ".open-next/server-functions/default");
|
|
825
1052
|
const zipPath = path.join(process.cwd(), ".open-next/function-app.zip");
|
|
826
|
-
await
|
|
1053
|
+
await fs.rm(zipPath, { force: true });
|
|
1054
|
+
await execAsync$1(`cd "${functionsPath}" && zip -r "${zipPath}" . -q`, {
|
|
827
1055
|
maxBuffer: 100 * 1024 * 1024
|
|
828
1056
|
});
|
|
829
1057
|
await execAsync$1(
|
|
830
|
-
`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}"`,
|
|
831
1059
|
{
|
|
832
1060
|
maxBuffer: 100 * 1024 * 1024
|
|
833
1061
|
}
|
|
@@ -835,6 +1063,41 @@ async function deployFunctionApp(functionAppName, resourceGroup) {
|
|
|
835
1063
|
await fs.unlink(zipPath);
|
|
836
1064
|
}
|
|
837
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
|
+
|
|
838
1101
|
const execAsync = promisify(exec);
|
|
839
1102
|
async function promptForInput(question) {
|
|
840
1103
|
const rl = readline.createInterface({
|
|
@@ -932,6 +1195,7 @@ async function deploy(options) {
|
|
|
932
1195
|
if (!resourceGroup) {
|
|
933
1196
|
const result = await selectResourceGroup();
|
|
934
1197
|
resourceGroup = result.name;
|
|
1198
|
+
validateAzureNames({ resourceGroup });
|
|
935
1199
|
if (result.isNew) {
|
|
936
1200
|
resourceGroupLocation = result.location;
|
|
937
1201
|
} else {
|
|
@@ -939,12 +1203,16 @@ async function deploy(options) {
|
|
|
939
1203
|
}
|
|
940
1204
|
}
|
|
941
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 });
|
|
942
1209
|
await deploy$1({
|
|
943
|
-
appName
|
|
1210
|
+
appName,
|
|
944
1211
|
resourceGroup,
|
|
945
|
-
location
|
|
1212
|
+
location,
|
|
946
1213
|
environment,
|
|
947
1214
|
skipInfrastructure: options.skipInfrastructure,
|
|
1215
|
+
skipResourceChecks: options.skipResourceChecks,
|
|
948
1216
|
applicationInsights: config.applicationInsights ?? false
|
|
949
1217
|
});
|
|
950
1218
|
}
|
|
@@ -960,4 +1228,4 @@ async function getResourceGroupLocation(resourceGroup) {
|
|
|
960
1228
|
}
|
|
961
1229
|
}
|
|
962
1230
|
|
|
963
|
-
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;
|
|
@@ -30,6 +31,7 @@ declare function deploy(options: {
|
|
|
30
31
|
location?: string;
|
|
31
32
|
environment?: "dev" | "staging" | "prod";
|
|
32
33
|
skipInfrastructure?: boolean;
|
|
34
|
+
skipResourceChecks?: boolean;
|
|
33
35
|
}): Promise<void>;
|
|
34
36
|
|
|
35
37
|
export { build, deploy, init };
|
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;
|
|
@@ -30,6 +31,7 @@ declare function deploy(options: {
|
|
|
30
31
|
location?: string;
|
|
31
32
|
environment?: "dev" | "staging" | "prod";
|
|
32
33
|
skipInfrastructure?: boolean;
|
|
34
|
+
skipResourceChecks?: boolean;
|
|
33
35
|
}): Promise<void>;
|
|
34
36
|
|
|
35
37
|
export { build, deploy, init };
|
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 };
|