Skip to content
This repository was archived by the owner on Feb 1, 2025. It is now read-only.

Commit 24c0310

Browse files
authored
feature(bumps): allow for cli to calculate and bump versions for projects, prettier config added (#17)
Co-authored-by: Jan Soukup <[email protected]>
1 parent 7fbc629 commit 24c0310

8 files changed

+407
-174
lines changed

Diff for: .gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ node_modules/**
22
.env*
33
!.env.example
44
dist/**
5+
6+
# Temporary build folder.
7+
nodejs/**

Diff for: .prettierrc

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"printWidth": 160,
3+
"useTabs": true,
4+
"singleQuote": true,
5+
"semi": false,
6+
"trailingComma": "all",
7+
"arrowParens": "always",
8+
"parser": "typescript"
9+
}

Diff for: lib/cli.ts

+125-35
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,139 @@
11
#!/usr/bin/env node
2-
import { exec as child_exec } from "child_process"
3-
import util from "util"
4-
import path from "path"
5-
import packageJson from "../package.json"
2+
import { exec as child_exec } from 'child_process'
3+
import util from 'util'
4+
import path from 'path'
5+
import packageJson from '../package.json'
6+
import { simpleGit } from 'simple-git'
7+
import { Command } from 'commander'
8+
import { bumpCalculator, bumpMapping, BumpType, isValidTag } from './utils'
69

710
const exec = util.promisify(child_exec)
811

912
const scriptDir = path.dirname(__filename)
1013
const scriptPath = path.resolve(`${scriptDir}/../../scripts/pack-nextjs.sh`)
1114
const handlerPath = path.resolve(`${scriptDir}/../server-handler/index.js`)
1215

13-
import { Command } from "commander"
1416
const program = new Command()
1517

16-
program.name(packageJson.name).description(packageJson.description).version(packageJson.version)
18+
program
19+
//
20+
.name(packageJson.name)
21+
.description(packageJson.description)
22+
.version(packageJson.version)
1723

1824
program
19-
.command("pack")
20-
.description("Package standalone Next12 build into Lambda compatible ZIPs.")
21-
.option("--output", "folder where to save output", "next.out")
22-
.option("--publicFolder", "folder where public assets are located", "public")
23-
.option("--handler", "custom handler to deal with ApiGw events", handlerPath)
24-
.option("--grepBy", "keyword to identify configuration inside server.js", "webpack")
25-
.action(async (str, options) => {
26-
// @TODO: Ensure path exists.
27-
// @TODO: Ensure.next folder exists with standalone folder inside.
28-
29-
// @TODO: Transform into code, move away from script.
30-
// Also, pass parameters and options.
31-
console.log("Starting packaging of your NextJS project!")
32-
33-
await exec(`chmod +x ${scriptPath} && ${scriptPath}`)
34-
.then(({ stdout }) => console.log(stdout))
35-
.catch(console.error)
36-
37-
console.log("Your NextJS project was succefully prepared for Lambda.")
38-
})
25+
.command('pack')
26+
.description('Package standalone Next12 build into Lambda compatible ZIPs.')
27+
.option('--output', 'folder where to save output', 'next.out')
28+
.option('--publicFolder', 'folder where public assets are located', 'public')
29+
.option('--handler', 'custom handler to deal with ApiGw events', handlerPath)
30+
.option('--grepBy', 'keyword to identify configuration inside server.js', 'webpack')
31+
.action(async (str, options) => {
32+
// @TODO: Ensure path exists.
33+
// @TODO: Ensure.next folder exists with standalone folder inside.
34+
35+
// @TODO: Transform into code, move away from script.
36+
// Also, pass parameters and options.
37+
console.log('Starting packaging of your NextJS project!')
38+
39+
await exec(`chmod +x ${scriptPath} && ${scriptPath}`)
40+
.then(({ stdout }) => console.log(stdout))
41+
.catch(console.error)
42+
43+
console.log('Your NextJS project was succefully prepared for Lambda.')
44+
})
3945

4046
program
41-
.command("guess")
42-
.description("Get commits from last tag and guess bump version based on SemVer/Chakra keywords.")
43-
.action(async (str, options) => {
44-
// @TODO: Implement git commits parsing.
45-
// Use ref, docs, bugfix, fix, feature, feat, etc.
46-
// Also consider parsing commit body for things such as "Breaking change"
47-
})
48-
49-
program.parse()
47+
.command('guess')
48+
.description('Calculate next version based on last version and commit message.')
49+
.argument('<commitMessage>', 'Commit message to use for guessing bump.')
50+
.argument('<latestVersion>', 'Your existing app version which should be used for calculation of next version.')
51+
.option('-t, --tagPrefix <prefix>', 'Prefix version with string of your choice', 'v')
52+
.action(async (args, options) => {
53+
const { commitMessage, latestVersion } = args
54+
const { tagPrefix } = options
55+
56+
if (!isValidTag(latestVersion, tagPrefix)) {
57+
throw new Error(`Invalid version found - ${latestVersion}!`)
58+
}
59+
60+
const match = bumpMapping.find(({ test }) => commitMessage.match(test))
61+
if (!match) {
62+
throw new Error('No mapping for for suplied commit message.')
63+
}
64+
65+
const nextTag = bumpCalculator(latestVersion.replace(tagPrefix, ''), match?.bump)
66+
const nextTagWithPrefix = tagPrefix + nextTag
67+
68+
console.log(nextTagWithPrefix)
69+
})
70+
71+
program
72+
.command('shipit')
73+
.description('Get last tag, calculate bump version for all commits that happened and create release branch.')
74+
.option('--failOnMissingCommit', 'In case commit has not happened since last tag (aka. we are on latest tag) fail.', Boolean, true)
75+
.option('-f, --forceBump', 'In case no compatible commits found, use patch as fallback and ensure bump happens.', Boolean, true)
76+
.option('-a, --autoPush', 'This will automatically create release branch and tag commit in master.', Boolean, true)
77+
.option('-t, --tagPrefix <prefix>', 'Prefix version with string of your choice', 'v')
78+
.option('-r, --releaseBranchPrefix <prefix>', 'Prefix for release branch fork.', 'release/')
79+
.action(async (options) => {
80+
const { tagPrefix, failOnMissingCommit, releaseBranchPrefix, forceBump } = options
81+
82+
const git = simpleGit()
83+
const tags = await git.tags()
84+
const log = await git.log()
85+
const [remote] = await git.getRemotes()
86+
87+
const latestCommit = log.latest?.hash
88+
const latestTag = tags.latest ?? '0.0.0'
89+
90+
if (!isValidTag(latestTag, tagPrefix)) {
91+
throw new Error(`Invalid tag found - ${latestTag}!`)
92+
}
93+
94+
if (!latestCommit) {
95+
throw new Error('Latest commit was not found!')
96+
}
97+
98+
const commits = await git.log({
99+
from: tags.latest,
100+
to: latestCommit,
101+
})
102+
103+
if (commits.total < 1 && failOnMissingCommit) {
104+
throw new Error('No new commits since last tag.')
105+
}
106+
107+
const bumps = []
108+
109+
commits.all.forEach(({ message, body }) => {
110+
const match = bumpMapping.find(({ test, scanBody }) => (scanBody ? body : message).match(test))
111+
if (!match) {
112+
console.warn(`Invalid commit, cannot match bump - ${message}!`)
113+
} else {
114+
bumps.push(match?.bump)
115+
}
116+
})
117+
118+
// Bump minor in case nothing is found.
119+
if (bumps.length < 1 && forceBump) {
120+
bumps.push(BumpType.Patch)
121+
}
122+
123+
const nextTag = bumps.reduce((acc, curr) => bumpCalculator(acc, curr), latestTag.replace(tagPrefix, ''))
124+
const nextTagWithPrefix = tagPrefix + nextTag
125+
const releaseBranch = `${releaseBranchPrefix}${nextTagWithPrefix}`
126+
127+
console.log(`Next version is - ${nextTagWithPrefix}!`)
128+
129+
// Create tag and push it to master.
130+
// @Note: CI/CD should not be listening for tags in master, it should listen to release branch.
131+
await git.addTag(nextTagWithPrefix)
132+
await git.pushTags()
133+
134+
await git.push(remote.name, releaseBranch)
135+
136+
console.log(`Successfuly tagged and created new branch - ${releaseBranch}`)
137+
})
138+
139+
program.parse(process.argv)

Diff for: lib/image-handler.ts

+46-51
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,65 @@
1-
process.env.NODE_ENV = "production"
1+
process.env.NODE_ENV = 'production'
22
// Set NEXT_SHARP_PATH environment variable
33
// ! Make sure this comes before the fist import
4-
process.env.NEXT_SHARP_PATH = require.resolve("sharp")
4+
process.env.NEXT_SHARP_PATH = require.resolve('sharp')
55

6-
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from "aws-lambda"
7-
import { defaultConfig, NextConfigComplete } from "next/dist/server/config-shared"
8-
import { imageOptimizer as nextImageOptimizer, ImageOptimizerCache } from "next/dist/server/image-optimizer"
9-
import { ImageConfigComplete } from "next/dist/shared/lib/image-config"
10-
import { normalizeHeaders, requestHandler } from "./utils"
6+
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda'
7+
import { defaultConfig, NextConfigComplete } from 'next/dist/server/config-shared'
8+
import { imageOptimizer as nextImageOptimizer, ImageOptimizerCache } from 'next/dist/server/image-optimizer'
9+
import { ImageConfigComplete } from 'next/dist/shared/lib/image-config'
10+
import { normalizeHeaders, requestHandler } from './utils'
1111

1212
const sourceBucket = process.env.S3_SOURCE_BUCKET ?? undefined
1313

1414
// @TODO: Allow passing params as env vars.
1515
const nextConfig = {
16-
...(defaultConfig as NextConfigComplete),
17-
images: {
18-
...(defaultConfig.images as ImageConfigComplete),
19-
// ...(domains && { domains }),
20-
// ...(deviceSizes && { deviceSizes }),
21-
// ...(formats && { formats }),
22-
// ...(imageSizes && { imageSizes }),
23-
// ...(dangerouslyAllowSVG && { dangerouslyAllowSVG }),
24-
// ...(contentSecurityPolicy && { contentSecurityPolicy }),
25-
},
16+
...(defaultConfig as NextConfigComplete),
17+
images: {
18+
...(defaultConfig.images as ImageConfigComplete),
19+
// ...(domains && { domains }),
20+
// ...(deviceSizes && { deviceSizes }),
21+
// ...(formats && { formats }),
22+
// ...(imageSizes && { imageSizes }),
23+
// ...(dangerouslyAllowSVG && { dangerouslyAllowSVG }),
24+
// ...(contentSecurityPolicy && { contentSecurityPolicy }),
25+
},
2626
}
2727

2828
const optimizer = async (event: APIGatewayProxyEventV2): Promise<APIGatewayProxyStructuredResultV2> => {
29-
try {
30-
if (!sourceBucket) {
31-
throw new Error("Bucket name must be defined!")
32-
}
29+
try {
30+
if (!sourceBucket) {
31+
throw new Error('Bucket name must be defined!')
32+
}
3333

34-
const imageParams = ImageOptimizerCache.validateParams(
35-
{ headers: event.headers } as any,
36-
event.queryStringParameters!,
37-
nextConfig,
38-
false
39-
)
34+
const imageParams = ImageOptimizerCache.validateParams({ headers: event.headers } as any, event.queryStringParameters!, nextConfig, false)
4035

41-
if ("errorMessage" in imageParams) {
42-
throw new Error(imageParams.errorMessage)
43-
}
36+
if ('errorMessage' in imageParams) {
37+
throw new Error(imageParams.errorMessage)
38+
}
4439

45-
const optimizedResult = await nextImageOptimizer(
46-
{ headers: normalizeHeaders(event.headers) } as any,
47-
{} as any, // res object is not necessary as it's not actually used.
48-
imageParams,
49-
nextConfig,
50-
requestHandler(sourceBucket)
51-
)
40+
const optimizedResult = await nextImageOptimizer(
41+
{ headers: normalizeHeaders(event.headers) } as any,
42+
{} as any, // res object is not necessary as it's not actually used.
43+
imageParams,
44+
nextConfig,
45+
requestHandler(sourceBucket),
46+
)
5247

53-
console.log(optimizedResult)
48+
console.log(optimizedResult)
5449

55-
return {
56-
statusCode: 200,
57-
body: optimizedResult.buffer.toString("base64"),
58-
isBase64Encoded: true,
59-
headers: { Vary: "Accept", "Content-Type": optimizedResult.contentType },
60-
}
61-
} catch (error: any) {
62-
console.error(error)
63-
return {
64-
statusCode: 500,
65-
body: error?.message || error?.toString() || error,
66-
}
67-
}
50+
return {
51+
statusCode: 200,
52+
body: optimizedResult.buffer.toString('base64'),
53+
isBase64Encoded: true,
54+
headers: { Vary: 'Accept', 'Content-Type': optimizedResult.contentType },
55+
}
56+
} catch (error: any) {
57+
console.error(error)
58+
return {
59+
statusCode: 500,
60+
body: error?.message || error?.toString() || error,
61+
}
62+
}
6863
}
6964

7065
export const handler = optimizer

Diff for: lib/server-handler.ts

+30-38
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,49 @@
1-
process.env.NODE_ENV = "production"
1+
process.env.NODE_ENV = 'production'
22
process.chdir(__dirname)
33

4-
import NextServer from "next/dist/server/next-server"
5-
import slsHttp from "serverless-http"
6-
import path from "path"
7-
import { ServerResponse } from "http"
4+
import NextServer from 'next/dist/server/next-server'
5+
import slsHttp from 'serverless-http'
6+
import path from 'path'
7+
import { ServerResponse } from 'http'
88

99
// This will be loaded from custom config parsed via CLI.
10-
const nextConf = require(`${process.env.NEXT_CONFIG_FILE ?? "./config.json"}`)
10+
const nextConf = require(`${process.env.NEXT_CONFIG_FILE ?? './config.json'}`)
1111

1212
// Make sure commands gracefully respect termination signals (e.g. from Docker)
1313
// Allow the graceful termination to be manually configurable
1414
if (!process.env.NEXT_MANUAL_SIG_HANDLE) {
15-
process.on("SIGTERM", () => process.exit(0))
16-
process.on("SIGINT", () => process.exit(0))
15+
process.on('SIGTERM', () => process.exit(0))
16+
process.on('SIGINT', () => process.exit(0))
1717
}
1818

1919
const config = {
20-
hostname: "localhost",
21-
port: Number(process.env.PORT) || 3000,
22-
dir: path.join(__dirname),
23-
dev: false,
24-
customServer: false,
25-
conf: nextConf,
20+
hostname: 'localhost',
21+
port: Number(process.env.PORT) || 3000,
22+
dir: path.join(__dirname),
23+
dev: false,
24+
customServer: false,
25+
conf: nextConf,
2626
}
2727

28+
const getErrMessage = (e: any) => ({ message: 'Server failed to respond.', details: e })
29+
2830
const nextHandler = new NextServer(config).getRequestHandler()
2931

3032
const server = slsHttp(
31-
async (req: any, res: ServerResponse) => {
32-
await nextHandler(req, res).catch((e) => {
33-
// Log into Cloudwatch for easier debugging.
34-
console.error(`NextJS request failed due to:`)
35-
console.error(e)
36-
37-
res.setHeader("Content-Type", "application/json")
38-
res.end(
39-
JSON.stringify(
40-
{
41-
message: "Server failed to respond.",
42-
details: e,
43-
},
44-
// Prettified.
45-
null,
46-
3
47-
)
48-
)
49-
})
50-
},
51-
{
52-
// We have separate function for handling images. Assets are handled by S3.
53-
binary: false,
54-
}
33+
async (req: any, res: ServerResponse) => {
34+
await nextHandler(req, res).catch((e) => {
35+
// Log into Cloudwatch for easier debugging.
36+
console.error(`NextJS request failed due to:`)
37+
console.error(e)
38+
39+
res.setHeader('Content-Type', 'application/json')
40+
res.end(JSON.stringify(getErrMessage(e), null, 3))
41+
})
42+
},
43+
{
44+
// We have separate function for handling images. Assets are handled by S3.
45+
binary: false,
46+
},
5547
)
5648

5749
export const handler = server

0 commit comments

Comments
 (0)