From 3988a44a208cb2e0a7d3007770fd8d065151850f Mon Sep 17 00:00:00 2001 From: Roman Bobrovskiy Date: Wed, 27 Nov 2024 17:32:11 +0100 Subject: [PATCH] feat: added next config wrapper utility --- package-lock.json | 2 +- package.json | 5 +- src/build/next.ts | 71 ++++++------------ src/build/withNextDeploy.ts | 21 ++++++ src/cacheHandler/index.ts | 3 +- src/cacheHandler/strategy/s3.spec.ts | 16 ++--- src/commands/deploy.ts | 15 ++-- src/commands/index.ts | 103 ++++++++++++++++++++++++++ src/common/aws.ts | 6 +- src/index.ts | 104 +-------------------------- 10 files changed, 172 insertions(+), 174 deletions(-) create mode 100644 src/build/withNextDeploy.ts create mode 100644 src/commands/index.ts diff --git a/package-lock.json b/package-lock.json index 2edf135..3dfafbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "yargs": "17.7.2" }, "bin": { - "next-serverless-deployment": "dist/index.js" + "next-serverless-deployment": "dist/commands/index.js" }, "devDependencies": { "@semantic-release/changelog": "6.0.3", diff --git a/package.json b/package.json index 33bd4f4..e033cef 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,7 @@ "description": "", "main": "./dist/index.js", "bin": { - "@dbbs/next-serverless-deployment": "./dist/index.js" - }, - "exports": { - "./*": "./dist/*" + "@dbbs/next-serverless-deployment": "./dist/commands/index.js" }, "types": "dist/types/index.d.ts", "files": [ diff --git a/src/build/next.ts b/src/build/next.ts index 9d86b96..5ae9940 100644 --- a/src/build/next.ts +++ b/src/build/next.ts @@ -1,9 +1,8 @@ import childProcess from 'node:child_process' import fs from 'fs/promises' import path from 'node:path' -import { type ProjectPackager, type ProjectSettings, loadFile } from '../common/project' -import loadConfig from '../commands/helpers/loadConfig' -import appRouterRevalidate from './cache/handlers/appRouterRevalidate' +import { type ProjectPackager, type ProjectSettings } from '../common/project' +import appRouterRevalidateTemplate from './cache/handlers/appRouterRevalidate' interface BuildOptions { packager: ProjectPackager @@ -21,78 +20,52 @@ interface BuildAppOptions { export const OUTPUT_FOLDER = 'serverless-next' -const setNextOptions = async (nextConfigPath: string, s3BucketName: string): Promise<() => Promise> => { - // set s3 bucket name for cache handler during build time +const setNextEnvs = (s3BucketName: string) => { process.env.STATIC_BUCKET_NAME = s3BucketName - - const cacheConfig = await loadConfig() - const currentConfig = await loadFile(nextConfigPath) - const updatedConfig = { - ...currentConfig, - output: 'standalone', - serverRuntimeConfig: { - ...currentConfig.serverRuntimeConfig, - nextServerlessCacheConfig: cacheConfig - }, - cacheHandler: require.resolve(path.join('..', 'cacheHandler', 'index.js')) - } - - const currentContent = await fs.readFile(nextConfigPath, 'utf-8') - - let updatedContent = `module.exports = ${JSON.stringify(updatedConfig, null, 4)};\n` - - // Check if the file has .mjs extension - if (nextConfigPath.endsWith('.mjs')) { - updatedContent = `export default ${JSON.stringify(updatedConfig, null, 4)};\n` - } - - await fs.writeFile(nextConfigPath, updatedContent, 'utf-8') - - // Function to revert back to original content of file - return async () => { - fs.writeFile(nextConfigPath, currentContent, 'utf-8') - } + process.env.NEXT_SERVERLESS_DEPLOYING_PHASE = 'true' } const appendRevalidateApi = async (projectPath: string, isAppDir: boolean): Promise => { const routeFolderPath = path.join(projectPath, isAppDir ? 'src/app' : 'src', 'api', 'revalidate') const routePath = path.join(routeFolderPath, 'route.ts') - if ((await fs.stat(routeFolderPath)).isDirectory()) { - await fs.mkdir(routeFolderPath, { recursive: true }) - } - fs.writeFile(routePath, appRouterRevalidate, 'utf-8') + await fs.mkdir(routeFolderPath, { recursive: true }) + await fs.writeFile(routePath, appRouterRevalidateTemplate, 'utf-8') return routePath } export const buildNext = async (options: BuildOptions): Promise<() => Promise> => { - const { packager, nextConfigPath, s3BucketName, projectPath, isAppDir } = options + const { packager, projectPath, s3BucketName, isAppDir } = options + setNextEnvs(s3BucketName) const revalidateRoutePath = await appendRevalidateApi(projectPath, isAppDir) - const clearNextConfig = await setNextOptions(nextConfigPath, s3BucketName) childProcess.execSync(packager.buildCommand, { stdio: 'inherit' }) // Reverts changes to the next project return async () => { - await Promise.all([clearNextConfig(), fs.rm(revalidateRoutePath)]) + await fs.rm(revalidateRoutePath) } } -const copyAssets = async (outputPath: string, appPath: string) => { +const copyAssets = async (outputPath: string, appPath: string, appRelativePath: string) => { // Copying static assets (like js, css, images, .etc) - await Promise.all([ - fs.cp(path.join(appPath, '.next', 'static'), path.join(outputPath, '_next', 'static'), { recursive: true }), - fs.cp(path.join(appPath, '.next', 'standalone'), path.join(outputPath, 'server'), { + await fs.cp(path.join(appPath, '.next'), path.join(outputPath, '.next'), { + recursive: true + }) + await fs.cp( + path.join(appPath, '.next', 'static'), + path.join(outputPath, '.next', 'standalone', appRelativePath, '.next', 'static'), + { recursive: true - }) - ]) + } + ) } export const buildApp = async (options: BuildAppOptions) => { const { projectSettings, outputPath, s3BucketName } = options - const { packager, nextConfigPath, projectPath, isAppDir } = projectSettings + const { packager, nextConfigPath, projectPath, isAppDir, root, isMonorepo } = projectSettings const cleanNextApp = await buildNext({ packager, @@ -102,7 +75,9 @@ export const buildApp = async (options: BuildAppOptions) => { projectPath }) - await copyAssets(outputPath, projectPath) + const appRelativePath = isMonorepo ? path.relative(root, projectPath) : '' + + await copyAssets(outputPath, projectPath, appRelativePath) return cleanNextApp } diff --git a/src/build/withNextDeploy.ts b/src/build/withNextDeploy.ts new file mode 100644 index 0000000..002b07d --- /dev/null +++ b/src/build/withNextDeploy.ts @@ -0,0 +1,21 @@ +import type { NextConfig } from 'next/dist/server/config-shared' +import path from 'node:path' +import loadConfig from '../commands/helpers/loadConfig' + +export const withNextDeploy = async (nextConfig: NextConfig): Promise => { + if (process.env.NEXT_SERVERLESS_DEPLOYING_PHASE === 'true') { + const cacheConfig = await loadConfig() + return { + ...nextConfig, + output: 'standalone', + serverRuntimeConfig: { + ...nextConfig.serverRuntimeConfig, + nextServerlessCacheConfig: cacheConfig, + staticBucketName: process.env.STATIC_BUCKET_NAME + }, + cacheHandler: require.resolve(path.join('..', 'cacheHandler', 'index.js')) + } + } + + return nextConfig +} diff --git a/src/cacheHandler/index.ts b/src/cacheHandler/index.ts index a674453..596d7a2 100644 --- a/src/cacheHandler/index.ts +++ b/src/cacheHandler/index.ts @@ -5,13 +5,14 @@ import { S3Cache } from './strategy/s3' const { serverRuntimeConfig } = getConfig() || {} const config: CacheConfig | undefined = serverRuntimeConfig?.nextServerlessCacheConfig +const staticBucketName = serverRuntimeConfig?.staticBucketName || '' Cache.setConfig({ cacheCookies: config?.cacheCookies ?? [], cacheQueries: config?.cacheQueries ?? [], noCacheMatchers: config?.noCacheRoutes ?? [], enableDeviceSplit: config?.enableDeviceSplit, - cache: new S3Cache(process.env.STATIC_BUCKET_NAME!) + cache: new S3Cache(staticBucketName) }) export default Cache diff --git a/src/cacheHandler/strategy/s3.spec.ts b/src/cacheHandler/strategy/s3.spec.ts index 39444f1..9021ad8 100644 --- a/src/cacheHandler/strategy/s3.spec.ts +++ b/src/cacheHandler/strategy/s3.spec.ts @@ -85,7 +85,7 @@ describe('S3Cache', () => { ContentType: 'application/json' }) - const result = await s3Cache.get(cacheKey, cacheKey, mockCacheContext) + const result = await s3Cache.get(cacheKey, cacheKey) expect(result).toEqual(mockCacheEntry.value.pageData) expect(s3Cache.client.getObject).toHaveBeenCalledTimes(1) expect(s3Cache.client.getObject).toHaveBeenCalledWith({ @@ -110,7 +110,7 @@ describe('S3Cache', () => { ContentType: 'text/x-component' }) - const result = await s3Cache.get(cacheKey, cacheKey, mockCacheContext) + const result = await s3Cache.get(cacheKey, cacheKey) expect(result).toEqual(mockCacheEntry.value.pageData) expect(s3Cache.client.getObject).toHaveBeenCalledTimes(1) expect(s3Cache.client.getObject).toHaveBeenCalledWith({ @@ -135,7 +135,7 @@ describe('S3Cache', () => { ContentType: 'application/json' }) - const result = await s3Cache.get(cacheKey, cacheKey, mockCacheContext) + const result = await s3Cache.get(cacheKey, cacheKey) expect(result).toEqual(mockCacheEntry.value.pageData) expect(s3Cache.client.getObject).toHaveBeenCalledTimes(1) expect(s3Cache.client.getObject).toHaveBeenCalledWith({ @@ -144,7 +144,7 @@ describe('S3Cache', () => { }) await s3Cache.delete(cacheKey, cacheKey) - const updatedResult = await s3Cache.get(cacheKey, cacheKey, mockCacheContext) + const updatedResult = await s3Cache.get(cacheKey, cacheKey) expect(updatedResult).toBeNull() expect(s3Cache.client.deleteObjects).toHaveBeenCalledTimes(1) expect(s3Cache.client.deleteObjects).toHaveBeenNthCalledWith(1, { @@ -163,19 +163,19 @@ describe('S3Cache', () => { const mockCacheEntryWithTags = { ...mockCacheEntry, tags: [cacheKey] } await s3Cache.set(cacheKey, cacheKey, mockCacheEntryWithTags, mockCacheContext) - expect(await s3Cache.get(cacheKey, cacheKey, mockCacheContext)).toEqual(mockCacheEntryWithTags.value.pageData) + expect(await s3Cache.get(cacheKey, cacheKey)).toEqual(mockCacheEntryWithTags.value.pageData) await s3Cache.revalidateTag(cacheKey, []) - expect(await s3Cache.get(cacheKey, cacheKey, mockCacheContext)).toBeNull() + expect(await s3Cache.get(cacheKey, cacheKey)).toBeNull() }) it('should revalidate cache by path', async () => { await s3Cache.set(cacheKey, cacheKey, mockCacheEntry, mockCacheContext) - expect(await s3Cache.get(cacheKey, cacheKey, mockCacheContext)).toEqual(mockCacheEntry.value.pageData) + expect(await s3Cache.get(cacheKey, cacheKey)).toEqual(mockCacheEntry.value.pageData) await s3Cache.deleteAllByKeyMatch(cacheKey, '') - expect(await s3Cache.get(cacheKey, cacheKey, mockCacheContext)).toBeNull() + expect(await s3Cache.get(cacheKey, cacheKey)).toBeNull() }) }) diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index de4113e..c5b7280 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -168,21 +168,24 @@ export const deploy = async (config: DeployConfig) => { const versionLabel = `${OUTPUT_FOLDER}-server-v${now}` fs.writeFileSync( - path.join(outputPath, 'server', 'Procfile'), + path.join(outputPath, '.next', 'Procfile'), `web: node ${path.join(path.relative(projectSettings.root, projectSettings.projectPath), 'server.js')}` ) - childProcess.execSync(`cd ${path.join(outputPath, 'server')} && zip -r ../${archivedFolderName} \\.* *`, { - stdio: 'inherit' - }) + childProcess.execSync( + `cd ${path.join(outputPath, '.next', 'standalone')} && zip -r ../../${archivedFolderName} \\.* *`, + { + stdio: 'inherit' + } + ) // prune static bucket before upload await emptyBucket(s3Client, nextRenderServerStackOutput.StaticBucketName) await uploadFolderToS3(s3Client, { Bucket: nextRenderServerStackOutput.StaticBucketName, - Key: '_next', - folderRootPath: outputPath + Key: '_next/static', + folderRootPath: path.join(outputPath, '.next', 'static') }) // upload code version to bucket. diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..9e707bb --- /dev/null +++ b/src/commands/index.ts @@ -0,0 +1,103 @@ +#!/usr/bin/env node +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' +import { deploy } from './deploy' +import { bootstrap } from './bootstrap' + +interface CLIOptions { + siteName: string + stage?: string + region?: string + profile?: string + nodejs?: string + production?: boolean + renderServerInstanceType?: string + renderServerMinInstances?: number + renderServerMaxInstances?: number +} + +const cli = yargs(hideBin(process.argv)) + .scriptName('@dbbs-next') + .usage('$0 [options]') + .option('region', { + type: 'string' + }) + .option('profile', { + type: 'string' + }) + +cli.command( + 'bootstrap', + 'bootsrap CDK project', + () => {}, + async (argv) => { + const { profile, region } = argv + await bootstrap({ profile, region }) + } +) + +cli + .command( + 'deploy', + 'app deployment', + () => {}, + async (argv) => { + const { + siteName, + stage, + region, + profile, + nodejs, + production, + renderServerInstanceType, + renderServerMinInstances, + renderServerMaxInstances + } = argv + + await deploy({ + siteName, + stage, + nodejs, + isProduction: production, + renderServerInstanceType, + renderServerMinInstances, + renderServerMaxInstances, + aws: { + region, + profile + } + }) + } + ) + .option('siteName', { + type: 'string', + requiresArg: true, + describe: 'The name is used to create CDK stack and components.' + }) + .option('stage', { + type: 'string', + describe: 'The stage of the app, defaults to production' + }) + .option('nodejs', { + type: 'string' + }) + .option('production', { + type: 'boolean', + description: 'Creates production stack.', + default: false + }) + .option('renderServerInstanceType', { + type: 'string', + describe: 'Set instance type for render server. Default is t2.micro.' + }) + .option('renderServerMinInstances', { + type: 'number', + describe: 'Set min render server instances. Default is 1.' + }) + .option('renderServerMaxInstances', { + type: 'number', + describe: 'Set max render server instances. Default is 2.' + }) + +cli.help() +cli.parse() diff --git a/src/common/aws.ts b/src/common/aws.ts index b69c7c6..3ceffac 100644 --- a/src/common/aws.ts +++ b/src/common/aws.ts @@ -69,17 +69,17 @@ export const uploadFileToS3 = async (s3Client: S3, options: PutObjectCommandInpu export const uploadFolderToS3 = async (s3Client: S3, options: S3UploadFolderOptions) => { const { folderRootPath, Key, ...s3UploadOptions } = options - const files = fs.readdirSync(path.join(folderRootPath, Key)) + const files = fs.readdirSync(folderRootPath) for (const file of files) { - const filePath = path.join(folderRootPath, Key, file) + const filePath = path.join(folderRootPath, file) const s3FilePath = path.join(Key, file) if (fs.lstatSync(filePath).isDirectory()) { await uploadFolderToS3(s3Client, { ...s3UploadOptions, Key: s3FilePath, - folderRootPath + folderRootPath: filePath }) } else { await uploadFileToS3(s3Client, { diff --git a/src/index.ts b/src/index.ts index 9994b77..a758851 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,103 +1 @@ -#!/usr/bin/env node -import yargs from 'yargs' -import { hideBin } from 'yargs/helpers' -import { deploy } from './commands/deploy' -import { bootstrap } from './commands/bootstrap' - -interface CLIOptions { - siteName: string - stage?: string - region?: string - profile?: string - nodejs?: string - production?: boolean - renderServerInstanceType?: string - renderServerMinInstances?: number - renderServerMaxInstances?: number -} - -const cli = yargs(hideBin(process.argv)) - .scriptName('@dbbs-next') - .usage('$0 [options]') - .option('region', { - type: 'string' - }) - .option('profile', { - type: 'string' - }) - -cli.command( - 'bootstrap', - 'bootsrap CDK project', - () => {}, - async (argv) => { - const { profile, region } = argv - await bootstrap({ profile, region }) - } -) - -cli - .command( - 'deploy', - 'app deployment', - () => {}, - async (argv) => { - const { - siteName, - stage, - region, - profile, - nodejs, - production, - renderServerInstanceType, - renderServerMinInstances, - renderServerMaxInstances - } = argv - - await deploy({ - siteName, - stage, - nodejs, - isProduction: production, - renderServerInstanceType, - renderServerMinInstances, - renderServerMaxInstances, - aws: { - region, - profile - } - }) - } - ) - .option('siteName', { - type: 'string', - requiresArg: true, - describe: 'The name is used to create CDK stack and components.' - }) - .option('stage', { - type: 'string', - describe: 'The stage of the app, defaults to production' - }) - .option('nodejs', { - type: 'string' - }) - .option('production', { - type: 'boolean', - description: 'Creates production stack.', - default: false - }) - .option('renderServerInstanceType', { - type: 'string', - describe: 'Set instance type for render server. Default is t2.micro.' - }) - .option('renderServerMinInstances', { - type: 'number', - describe: 'Set min render server instances. Default is 1.' - }) - .option('renderServerMaxInstances', { - type: 'number', - describe: 'Set max render server instances. Default is 2.' - }) - -cli.help() -cli.parse() +export * from './build/withNextDeploy'