Published: Sep 19, 2023 by Isaac Johnson
Coming back to the Vercel based React Nodesite app from last week, I’ll work on adding proper linting. The pnpm already had next lint, but we’ll add in BiomeJS for formatting, linting and more. Then tie that into the CICD pipeline. Lastly, we’ll work out a proper Continuous Deployment flow while using tools from the last blog like K1s, KDash and K9s.
Installing Biome
Let’s first install Biome.
I’ll set to at least NodeJS 16.14
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ nvm use 16.14.2
Now using node v16.14.2 (npm v8.5.0)
In this host, I lacked pnpm, so I added it back
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ npm install -g pnpm
changed 1 package, and audited 2 packages in 1s
1 package is looking for funding
run `npm fund` for details
found 0 vulnerabilities
npm notice
npm notice New major version of npm available! 8.5.0 -> 10.1.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.1.0
npm notice Run npm install -g npm@10.1.0 to update!
npm notice
I could then install Biome and save it to the packages.json
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm add --save-dev --save-exact @biomejs/biome
Packages: +2
++
Progress: resolved 357, reused 342, downloaded 2, added 2, done
Downloading registry.npmjs.org/@biomejs/cli-linux-x64/1.1.2: 5.90 MB/5.90 MB, done
node_modules/.pnpm/@biomejs+biome@1.1.2/node_modules/@biomejs/biome: Running postinstall script, done in 48ms
devDependencies:
+ @biomejs/biome 1.1.2
Done in 3s
Running
If you just try and run it, you’ll see that it desires a command on all operations
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm biome
flags/invalid ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Failed to parse CLI arguments.
Caused by:
expected `COMMAND ...`, pass `--help` for usage information
Our first step is to init
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm biome init
Welcome to Biome! Let's get you started...
Files created ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- biome.json: Your project configuration. Documentation: https://biomejs.dev/configuration
Next Steps ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Setup an editor extension
Get live errors as you type and format when you save. Learn more: https://biomejs.dev/editors
2. Try a command
biome ci checks for lint errors and verifies formatting. Run biome --help for a full list of commands and options.
3. Read the documentation
Our website serves as a comprehensive source of guides and documentation: https://biomejs.dev
4. Get involved in the community
Ask questions, get support, or contribute by participating on GitHub (https://github.com/biomejs/biome),
or join our community Discord (https://discord.gg/BypW39g6Yc)
The initial JSON is pretty straightforward:
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ cat biome.json
{
"$schema": "https://biomejs.dev/schemas/1.1.2/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}
CI Linting
Running the first time through found quite a lot:
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm biome ci --formatter-enabled=true --linter-enabled=true ./src/
./src/app/components/common/index.ts format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ File content differs from formatting output
2 2 │ // export * from "./Footer";
3 3 │ export * from "./Spinner";
4 │ - //·export·*·from·"./SocialHome";
4 │ + //·export·*·from·"./SocialHome";
5 │ +
./src/app/components/common/index.ts ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/guestbook/page.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
1 │ - import·"../globals.css";
2 │ - import·NavBar·from·"@/app/components/Navbar";
1 │ + import·NavBar·from·"@/app/components/Navbar";
2 │ + import·"../globals.css";
3 3 │
4 4 │ export const metadata = {
./src/app/components/common/UnderLine.tsx:4:20 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
2 │ return (
3 │ <div
> 4 │ className={`items-center h-0 border-t md:flex md:justify-between border-theme-600 my-4`}
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5 │ />
6 │ );
ℹ Suggested fix: Replace with string literal
2 2 │ return (
3 3 │ <div
4 │ - ········className={`items-center·h-0·border-t·md:flex·md:justify-between·border-theme-600·my-4`}
4 │ + ········className={"items-center·h-0·border-t·md:flex·md:justify-between·border-theme-600·my-4"}
5 5 │ />
6 6 │ );
./src/app/components/common/UnderLine.tsx lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/fun/page.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
1 │ - import·"../globals.css";
2 │ - import·NavBar·from·"@/app/components/Navbar";
1 │ + import·NavBar·from·"@/app/components/Navbar";
2 │ + import·"../globals.css";
3 3 │
4 4 │ export const metadata = {
./src/app/types/index.d.ts format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ File content differs from formatting output
1 1 │ export interface postMetadata {
2 │ - ····title:·string;
3 │ - ····subtitle:·string;
4 │ - ····date:·string;
5 │ - ····slug:·string;
6 │ - ····tags:·string[];
7 │ - ··}
8 │ - ··
9 │ - ··export·interface·portfolioMetadata·{
10 │ - ····title:·string;
11 │ - ····subtitle:·string;
12 │ - ····date:·sting;
13 │ - ····slug:·string;
14 │ - ····previewImg:·string;
15 │ - ··}
16 │ - ··
17 │ - ··export·interface·icon·{
18 │ - ····size?:·number;
19 │ - ··}
2 │ + → title:·string;
3 │ + → subtitle:·string;
4 │ + → date:·string;
5 │ + → slug:·string;
6 │ + → tags:·string[];
7 │ + }
8 │ +
9 │ + export·interface·portfolioMetadata·{
10 │ + → title:·string;
11 │ + → subtitle:·string;
12 │ + → date:·sting;
13 │ + → slug:·string;
14 │ + → previewImg:·string;
15 │ + }
16 │ +
17 │ + export·interface·icon·{
18 │ + → size?:·number;
19 │ + }
20 │ +
./src/app/types/index.d.ts ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/blog/page.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
1 │ - import·"../globals.css";
2 │ - import·NavBar·from·"@/app/components/Navbar";
1 │ + import·NavBar·from·"@/app/components/Navbar";
2 │ + import·"../globals.css";
3 3 │
4 4 │
5 │ - import·PostPreview·from·"@/app/components/preview/PostPreview";
6 │ - import·UnderLine·from·"@/app/components/common/UnderLine";
5 │ + import·UnderLine·from·"@/app/components/common/UnderLine";
6 │ + import·PostPreview·from·"@/app/components/preview/PostPreview";
7 7 │ import getPostMetadata from "@/app/libs/Metadata/getPostMetadata";
8 8 │ import PageWrapper from "@/app/providers/PageWrapper";
./src/app/layout.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
1 │ - import·"./globals.css";
2 │ - import·NavBar·from·"@/app/components/Navbar";
1 │ + import·NavBar·from·"@/app/components/Navbar";
2 │ + import·"./globals.css";
3 3 │
4 4 │ export const metadata = {
./src/app/libs/twclsx.ts organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
1 1 │ import clsx from "clsx";
2 │ - import·{·twMerge·}·from·"tailwind-merge";
3 │ - import·type·{·ClassValue·}·from·'clsx'
2 │ + import·type·{·ClassValue·}·from·'clsx'
3 │ + import·{·twMerge·}·from·"tailwind-merge";
4 4 │
5 5 │ export const twclsx = (...args: ClassValue[]) => twMerge(clsx(...args))
./src/app/components/common/UnderLine.tsx format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ File content differs from formatting output
1 1 │ const UnderLine = () => {
2 │ - ····return·(
3 │ - ······<div
4 │ - ········className={`items-center·h-0·border-t·md:flex·md:justify-between·border-theme-600·my-4`}
5 │ - ······/>
6 │ - ····);
7 │ - ··};
8 │ - ··
9 │ - ··export·default·UnderLine;
2 │ + → return·(
3 │ + → → <div
4 │ + → → → className={`items-center·h-0·border-t·md:flex·md:justify-between·border-theme-600·my-4`}
5 │ + → → />
6 │ + → );
7 │ + };
8 │ +
9 │ + export·default·UnderLine;
10 │ +
./src/app/components/common/UnderLine.tsx ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/providers/PageWrapper.tsx format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ File content differs from formatting output
2 2 │ import { motion } from "framer-motion";
3 3 │ const PageWrapper = ({ children }: { children: React.ReactNode }) => {
4 │ - ··return·(
5 │ - ····<motion.div
6 │ - ······initial={{·opacity:·0,·y:·30·}}
7 │ - ······animate={{·opacity:·1,·y:·0·}}
8 │ - ······exit={{·opacity:·0,·y:·30·}}
9 │ - ····>
10 │ - ······{children}
11 │ - ····</motion.div>
12 │ - ··);
4 │ + → return·(
5 │ + → → <motion.div
6 │ + → → → initial={{·opacity:·0,·y:·30·}}
7 │ + → → → animate={{·opacity:·1,·y:·0·}}
8 │ + → → → exit={{·opacity:·0,·y:·30·}}
9 │ + → → >
10 │ + → → → {children}
11 │ + → → </motion.div>
12 │ + → );
13 13 │ };
14 14 │
15 │ - export·default·PageWrapper;
15 │ + export·default·PageWrapper;
16 │ +
./src/app/guestbook/page.tsx format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ File content differs from formatting output
3 3 │
4 4 │ export const metadata = {
5 │ - ··title:·"Create·NextJS·App",
6 │ - ··description:·"Generated·by·create·next·app",
5 │ + → title:·"Create·NextJS·App",
6 │ + → description:·"Generated·by·create·next·app",
7 7 │ };
8 8 │
9 9 │ // <!-- use http://caius.github.io/github_id/ -->
10 10 │ const page = () => {
11 │ - ··return·(
12 │ - ············<div·className="flex·flex-col·gap-2·justify-center">
13 │ - ··············<h2·className="mb-0·text-zinc-100·font-bold">and</h2>
14 │ - ··············<p·className="mb-0·text-zinc-400·font-semibold·leading-none">
15 │ - ················....·guestbook·here·.....
16 │ - ··············</p>
17 │ - ············</div>
18 │ - ··);
19 │ - }
11 │ + → return·(
12 │ + → → <div·className="flex·flex-col·gap-2·justify-center">
13 │ + → → → <h2·className="mb-0·text-zinc-100·font-bold">and</h2>
14 │ + → → → <p·className="mb-0·text-zinc-400·font-semibold·leading-none">
15 │ + → → → → ....·guestbook·here·.....
16 │ + → → → </p>
17 │ + → → </div>
18 │ + → );
19 │ + };
20 20 │
21 │ - export·default·page;
21 │ + export·default·page;
22 │ +
./src/app/fun/page.tsx format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ File content differs from formatting output
3 3 │
4 4 │ export const metadata = {
5 │ - ··title:·"Create·NextJS·App",
6 │ - ··description:·"Generated·by·create·next·app",
5 │ + → title:·"Create·NextJS·App",
6 │ + → description:·"Generated·by·create·next·app",
7 7 │ };
8 8 │
9 9 │ // <!-- use http://caius.github.io/github_id/ -->
10 10 │ const page = () => {
11 │ - ··return·(
12 │ - ············<div·className="flex·flex-col·gap-2·justify-center">
13 │ - ··············<h2·className="mb-0·text-zinc-100·font-bold">and</h2>
14 │ - ··············<p·className="mb-0·text-zinc-400·font-semibold·leading-none">
15 │ - ················....·fun·here·.....
16 │ - ··············</p>
17 │ - ············</div>
18 │ - ··);
19 │ - }
11 │ + → return·(
12 │ + → → <div·className="flex·flex-col·gap-2·justify-center">
13 │ + → → → <h2·className="mb-0·text-zinc-100·font-bold">and</h2>
14 │ + → → → <p·className="mb-0·text-zinc-400·font-semibold·leading-none">
15 │ + → → → → ....·fun·here·.....
16 │ + → → → </p>
17 │ + → → </div>
18 │ + → );
19 │ + };
20 20 │
21 │ - export·default·page;
21 │ + export·default·page;
22 │ +
./src/app/providers/PageWrapper.tsx ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/guestbook/page.tsx ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/fun/page.tsx ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/libs/twclsx.ts format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ File content differs from formatting output
1 1 │ import clsx from "clsx";
2 2 │ import { twMerge } from "tailwind-merge";
3 │ - import·type·{·ClassValue·}·from·'clsx'
3 │ + import·type·{·ClassValue·}·from·"clsx";
4 4 │
5 │ - export·const·twclsx·=·(...args:·ClassValue[])·=>·twMerge(clsx(...args))
5 │ + export·const·twclsx·=·(...args:·ClassValue[])·=>·twMerge(clsx(...args));
6 │ +
./src/app/libs/twclsx.ts ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/libs/Metadata/getPostMetadata.ts:18:44 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━
✖ Unexpected any. Specify a different type.
16 │ subtitle: matterResult.data.subtitle,
17 │ slug: fileName.replace(".md", ""),
> 18 │ tags: matterResult.data.tags.map((e: any) => e)
│ ^^^
19 │ };
20 │ });
ℹ any disables many type checking rules. Its use should be avoided.
./src/app/blog/[slug]/page.tsx:24:26 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Unexpected any. Specify a different type.
22 │ };
23 │
> 24 │ const PostPage = (props: any) => {
│ ^^^
25 │ const slug = props.params.slug;
26 │ const post = getPostContent(slug);
ℹ any disables many type checking rules. Its use should be avoided.
./src/app/components/common/Tag.tsx:3:40 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Unexpected any. Specify a different type.
1 │ import clsx from "clsx";
2 │
> 3 │ const Tag = ({ tagString, className }: any) => {
│ ^^^
4 │ return (
5 │ <div className={clsx(`px-1 rounded-sm bg-theme-500 ${className}`)}>
ℹ any disables many type checking rules. Its use should be avoided.
./src/app/page.tsx:25:26 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
23 │ rel="noopener noreferrer"
24 │ >
> 25 │ <h2 className={`mb-3 text-2xl font-semibold`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26 │ Docs{' '}
27 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
ℹ Suggested fix: Replace with string literal
23 23 │ rel="noopener noreferrer"
24 24 │ >
25 │ - ··········<h2·className={`mb-3·text-2xl·font-semibold`}>
25 │ + ··········<h2·className={"mb-3·text-2xl·font-semibold"}>
26 26 │ Docs{' '}
27 27 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
./src/app/page.tsx:31:25 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
29 │ </span>
30 │ </h2>
> 31 │ <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
32 │ Find in-depth information about Next.js features and API.
33 │ </p>
ℹ Suggested fix: Replace with string literal
29 29 │ </span>
30 30 │ </h2>
31 │ - ··········<p·className={`m-0·max-w-[30ch]·text-sm·opacity-50`}>
31 │ + ··········<p·className={"m-0·max-w-[30ch]·text-sm·opacity-50"}>
32 32 │ Find in-depth information about Next.js features and API.
33 33 │ </p>
./src/app/page.tsx:42:26 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
40 │ rel="noopener noreferrer"
41 │ >
> 42 │ <h2 className={`mb-3 text-2xl font-semibold`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
43 │ Learn{' '}
44 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
ℹ Suggested fix: Replace with string literal
40 40 │ rel="noopener noreferrer"
41 41 │ >
42 │ - ··········<h2·className={`mb-3·text-2xl·font-semibold`}>
42 │ + ··········<h2·className={"mb-3·text-2xl·font-semibold"}>
43 43 │ Learn{' '}
44 44 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
./src/app/page.tsx:48:25 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
46 │ </span>
47 │ </h2>
> 48 │ <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49 │ Learn about Next.js in an interactive course with quizzes!
50 │ </p>
ℹ Suggested fix: Replace with string literal
46 46 │ </span>
47 47 │ </h2>
48 │ - ··········<p·className={`m-0·max-w-[30ch]·text-sm·opacity-50`}>
48 │ + ··········<p·className={"m-0·max-w-[30ch]·text-sm·opacity-50"}>
49 49 │ Learn about Next.js in an interactive course with quizzes!
50 50 │ </p>
./src/app/page.tsx:59:26 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
57 │ rel="noopener noreferrer"
58 │ >
> 59 │ <h2 className={`mb-3 text-2xl font-semibold`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
60 │ Templates{' '}
61 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
ℹ Suggested fix: Replace with string literal
57 57 │ rel="noopener noreferrer"
58 58 │ >
59 │ - ··········<h2·className={`mb-3·text-2xl·font-semibold`}>
59 │ + ··········<h2·className={"mb-3·text-2xl·font-semibold"}>
60 60 │ Templates{' '}
61 61 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
./src/app/page.tsx:65:25 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
63 │ </span>
64 │ </h2>
> 65 │ <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 │ Explore the Next.js 13 playground.
67 │ </p>
ℹ Suggested fix: Replace with string literal
63 63 │ </span>
64 64 │ </h2>
65 │ - ··········<p·className={`m-0·max-w-[30ch]·text-sm·opacity-50`}>
65 │ + ··········<p·className={"m-0·max-w-[30ch]·text-sm·opacity-50"}>
66 66 │ Explore the Next.js 13 playground.
67 67 │ </p>
./src/app/page.tsx:76:26 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
74 │ rel="noopener noreferrer"
75 │ >
> 76 │ <h2 className={`mb-3 text-2xl font-semibold`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
77 │ Deploy{' '}
78 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
ℹ Suggested fix: Replace with string literal
74 74 │ rel="noopener noreferrer"
75 75 │ >
76 │ - ··········<h2·className={`mb-3·text-2xl·font-semibold`}>
76 │ + ··········<h2·className={"mb-3·text-2xl·font-semibold"}>
77 77 │ Deploy{' '}
78 78 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
./src/app/page.tsx:82:25 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
80 │ </span>
81 │ </h2>
> 82 │ <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
83 │ Instantly deploy your Next.js site to a shareable URL with Vercel.
84 │ </p>
ℹ Suggested fix: Replace with string literal
80 80 │ </span>
81 81 │ </h2>
82 │ - ··········<p·className={`m-0·max-w-[30ch]·text-sm·opacity-50`}>
82 │ + ··········<p·className={"m-0·max-w-[30ch]·text-sm·opacity-50"}>
83 83 │ Instantly deploy your Next.js site to a shareable URL with Vercel.
84 84 │ </p>
Checked 16 file(s) in 6ms
Found 57 error(s)
ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Some errors were emitted while running checks.
Assuming I wish to fix these things, I could format them
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm biome format ./src/ --write
Formatted 16 file(s) in 2ms
rechecking, we do see a reduction from 57 errors to 33
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm biome ci --formatter-enabled=true --linter-enabled=true ./src/
./src/app/libs/twclsx.ts organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
1 1 │ import clsx from "clsx";
2 │ - import·{·twMerge·}·from·"tailwind-merge";
3 │ - import·type·{·ClassValue·}·from·"clsx";
2 │ + import·type·{·ClassValue·}·from·"clsx";
3 │ + import·{·twMerge·}·from·"tailwind-merge";
4 4 │
5 5 │ export const twclsx = (...args: ClassValue[]) => twMerge(clsx(...args));
./src/app/guestbook/page.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
1 │ - import·"../globals.css";
2 │ - import·NavBar·from·"@/app/components/Navbar";
1 │ + import·NavBar·from·"@/app/components/Navbar";
2 │ + import·"../globals.css";
3 3 │
4 4 │ export const metadata = {
./src/app/libs/twclsx.ts ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/libs/Metadata/getPostMetadata.ts:18:41 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━
✖ Unexpected any. Specify a different type.
16 │ subtitle: matterResult.data.subtitle,
17 │ slug: fileName.replace(".md", ""),
> 18 │ tags: matterResult.data.tags.map((e: any) => e),
│ ^^^
19 │ };
20 │ });
ℹ any disables many type checking rules. Its use should be avoided.
./src/app/components/common/UnderLine.tsx:4:15 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
2 │ return (
3 │ <div
> 4 │ className={`items-center h-0 border-t md:flex md:justify-between border-theme-600 my-4`}
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5 │ />
6 │ );
ℹ Suggested fix: Replace with string literal
2 2 │ return (
3 3 │ <div
4 │ - → → → className={`items-center·h-0·border-t·md:flex·md:justify-between·border-theme-600·my-4`}
4 │ + → → → className={"items-center·h-0·border-t·md:flex·md:justify-between·border-theme-600·my-4"}
5 5 │ />
6 6 │ );
./src/app/components/common/UnderLine.tsx lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/libs/Metadata/getPostMetadata.ts lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/components/common/Tag.tsx:3:40 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Unexpected any. Specify a different type.
1 │ import clsx from "clsx";
2 │
> 3 │ const Tag = ({ tagString, className }: any) => {
│ ^^^
4 │ return (
5 │ <div className={clsx(`px-1 rounded-sm bg-theme-500 ${className}`)}>
ℹ any disables many type checking rules. Its use should be avoided.
./src/app/components/common/Tag.tsx lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/libs/Metadata/getPostMetadata.ts organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
1 1 │ import fs from "fs";
2 │ - import·{·postMetadata·}·from·"../../types";
3 │ - import·matter·from·"gray-matter";
2 │ + import·matter·from·"gray-matter";
3 │ + import·{·postMetadata·}·from·"../../types";
4 4 │
5 5 │ const getPostMetadata = (): postMetadata[] => {
./src/app/blog/page.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
1 │ - import·"../globals.css";
2 │ - import·NavBar·from·"@/app/components/Navbar";
1 │ + import·NavBar·from·"@/app/components/Navbar";
2 │ + import·"../globals.css";
3 3 │
4 │ - import·PostPreview·from·"@/app/components/preview/PostPreview";
5 │ - import·UnderLine·from·"@/app/components/common/UnderLine";
4 │ + import·UnderLine·from·"@/app/components/common/UnderLine";
5 │ + import·PostPreview·from·"@/app/components/preview/PostPreview";
6 6 │ import getPostMetadata from "@/app/libs/Metadata/getPostMetadata";
7 7 │ import PageWrapper from "@/app/providers/PageWrapper";
./src/app/layout.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
1 │ - import·"./globals.css";
2 │ - import·NavBar·from·"@/app/components/Navbar";
1 │ + import·NavBar·from·"@/app/components/Navbar";
2 │ + import·"./globals.css";
3 3 │
4 4 │ export const metadata = {
./src/app/guestbook/page.tsx ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/components/Navbar.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
2 2 │
3 3 │ import { motion } from "framer-motion";
4 │ - import·{·useState·}·from·"react";
4 │ + import·Link·from·"next/link";
5 5 │ import { usePathname } from "next/navigation";
6 │ - import·Link·from·"next/link";
6 │ + import·{·useState·}·from·"react";
7 7 │
8 8 │ const navItems = [
./src/app/layout.tsx ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/blog/page.tsx ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/libs/Metadata/getPostMetadata.ts ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/blog/[slug]/page.tsx:24:26 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Unexpected any. Specify a different type.
22 │ };
23 │
> 24 │ const PostPage = (props: any) => {
│ ^^^
25 │ const slug = props.params.slug;
26 │ const post = getPostContent(slug);
ℹ any disables many type checking rules. Its use should be avoided.
./src/app/blog/[slug]/page.tsx lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/components/Navbar.tsx ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/blog/[slug]/page.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
1 1 │ import fs from "fs";
2 │ - import·matter·from·"gray-matter";
2 │ + import·Tag·from·"@/app/components/common/Tag";
3 │ + import·getPostMetadata·from·"@/app/libs/Metadata/getPostMetadata";
4 │ + import·matter·from·"gray-matter";
3 5 │ import Markdown from "markdown-to-jsx";
4 │ - import·getPostMetadata·from·"@/app/libs/Metadata/getPostMetadata";
5 │ - import·Link·from·"next/link";
6 │ - import·{·AiOutlineCaretLeft·}·from·"react-icons/ai";
7 │ - import·Tag·from·"@/app/components/common/Tag";
6 │ + import·Link·from·"next/link";
7 │ + import·{·AiOutlineCaretLeft·}·from·"react-icons/ai";
8 8 │
9 9 │ const getPostContent = (slug: string) => {
./src/app/fun/page.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Import statements differs from the output
1 │ - import·"../globals.css";
2 │ - import·NavBar·from·"@/app/components/Navbar";
1 │ + import·NavBar·from·"@/app/components/Navbar";
2 │ + import·"../globals.css";
3 3 │
4 4 │ export const metadata = {
./src/app/fun/page.tsx ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/blog/[slug]/page.tsx ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/page.tsx:24:21 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
22 │ rel="noopener noreferrer"
23 │ >
> 24 │ <h2 className={`mb-3 text-2xl font-semibold`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
25 │ Docs{" "}
26 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
ℹ Suggested fix: Replace with string literal
22 22 │ rel="noopener noreferrer"
23 23 │ >
24 │ - → → → → → <h2·className={`mb-3·text-2xl·font-semibold`}>
24 │ + → → → → → <h2·className={"mb-3·text-2xl·font-semibold"}>
25 25 │ Docs{" "}
26 26 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
./src/app/page.tsx:30:20 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
28 │ </span>
29 │ </h2>
> 30 │ <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
31 │ Find in-depth information about Next.js features and API.
32 │ </p>
ℹ Suggested fix: Replace with string literal
28 28 │ </span>
29 29 │ </h2>
30 │ - → → → → → <p·className={`m-0·max-w-[30ch]·text-sm·opacity-50`}>
30 │ + → → → → → <p·className={"m-0·max-w-[30ch]·text-sm·opacity-50"}>
31 31 │ Find in-depth information about Next.js features and API.
32 32 │ </p>
./src/app/page.tsx:41:21 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
39 │ rel="noopener noreferrer"
40 │ >
> 41 │ <h2 className={`mb-3 text-2xl font-semibold`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42 │ Learn{" "}
43 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
ℹ Suggested fix: Replace with string literal
39 39 │ rel="noopener noreferrer"
40 40 │ >
41 │ - → → → → → <h2·className={`mb-3·text-2xl·font-semibold`}>
41 │ + → → → → → <h2·className={"mb-3·text-2xl·font-semibold"}>
42 42 │ Learn{" "}
43 43 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
./src/app/page.tsx:47:20 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
45 │ </span>
46 │ </h2>
> 47 │ <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
48 │ Learn about Next.js in an interactive course with quizzes!
49 │ </p>
ℹ Suggested fix: Replace with string literal
45 45 │ </span>
46 46 │ </h2>
47 │ - → → → → → <p·className={`m-0·max-w-[30ch]·text-sm·opacity-50`}>
47 │ + → → → → → <p·className={"m-0·max-w-[30ch]·text-sm·opacity-50"}>
48 48 │ Learn about Next.js in an interactive course with quizzes!
49 49 │ </p>
./src/app/page.tsx:58:21 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
56 │ rel="noopener noreferrer"
57 │ >
> 58 │ <h2 className={`mb-3 text-2xl font-semibold`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59 │ Templates{" "}
60 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
ℹ Suggested fix: Replace with string literal
56 56 │ rel="noopener noreferrer"
57 57 │ >
58 │ - → → → → → <h2·className={`mb-3·text-2xl·font-semibold`}>
58 │ + → → → → → <h2·className={"mb-3·text-2xl·font-semibold"}>
59 59 │ Templates{" "}
60 60 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
./src/app/page.tsx:64:20 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
62 │ </span>
63 │ </h2>
> 64 │ <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
65 │ Explore the Next.js 13 playground.
66 │ </p>
ℹ Suggested fix: Replace with string literal
62 62 │ </span>
63 63 │ </h2>
64 │ - → → → → → <p·className={`m-0·max-w-[30ch]·text-sm·opacity-50`}>
64 │ + → → → → → <p·className={"m-0·max-w-[30ch]·text-sm·opacity-50"}>
65 65 │ Explore the Next.js 13 playground.
66 66 │ </p>
./src/app/page.tsx:75:21 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
73 │ rel="noopener noreferrer"
74 │ >
> 75 │ <h2 className={`mb-3 text-2xl font-semibold`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
76 │ Deploy{" "}
77 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
ℹ Suggested fix: Replace with string literal
73 73 │ rel="noopener noreferrer"
74 74 │ >
75 │ - → → → → → <h2·className={`mb-3·text-2xl·font-semibold`}>
75 │ + → → → → → <h2·className={"mb-3·text-2xl·font-semibold"}>
76 76 │ Deploy{" "}
77 77 │ <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
./src/app/page.tsx:81:20 lint/style/noUnusedTemplateLiteral FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Do not use template literals if interpolation and special-character handling are not needed.
79 │ </span>
80 │ </h2>
> 81 │ <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82 │ Instantly deploy your Next.js site to a shareable URL with Vercel.
83 │ </p>
ℹ Suggested fix: Replace with string literal
79 79 │ </span>
80 80 │ </h2>
81 │ - → → → → → <p·className={`m-0·max-w-[30ch]·text-sm·opacity-50`}>
81 │ + → → → → → <p·className={"m-0·max-w-[30ch]·text-sm·opacity-50"}>
82 82 │ Instantly deploy your Next.js site to a shareable URL with Vercel.
83 83 │ </p>
Checked 16 file(s) in 7ms
Found 33 error(s)
ci ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Some errors were emitted while running checks.
I also have evidence of the changes in my changes ready for staging
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git status
On branch linter
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: package.json
modified: pnpm-lock.yaml
modified: src/app/blog/[slug]/page.tsx
modified: src/app/blog/page.tsx
modified: src/app/components/Navbar.tsx
modified: src/app/components/common/Spinner.tsx
modified: src/app/components/common/Tag.tsx
modified: src/app/components/common/UnderLine.tsx
modified: src/app/components/common/index.ts
modified: src/app/components/preview/PostPreview.tsx
modified: src/app/fun/page.tsx
modified: src/app/guestbook/page.tsx
modified: src/app/layout.tsx
modified: src/app/libs/Metadata/getPostMetadata.ts
modified: src/app/libs/twclsx.ts
modified: src/app/page.tsx
modified: src/app/providers/PageWrapper.tsx
modified: src/app/types/index.d.ts
Untracked files:
(use "git add <file>..." to include in what will be committed)
biome.json
no changes added to commit (use "git add" and/or "git commit -a")
such as
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git diff src/app/components/preview/PostPreview.tsx
diff --git a/src/app/components/preview/PostPreview.tsx b/src/app/components/preview/PostPreview.tsx
index 14ed857..5350dfe 100644
--- a/src/app/components/preview/PostPreview.tsx
+++ b/src/app/components/preview/PostPreview.tsx
@@ -2,28 +2,22 @@ import Link from "next/link";
import { postMetadata } from "../../types/index";
const PostPreview = (props: postMetadata) => {
- return (
- <div
- key={props.slug}
- className="mb-0 text-zinc-400 px-2 py-1 transition ease-in-out rounded-lg hover:dark:bg-[#313131] hover:bg-primary-100 hover:-translate-y-1 cursor-pointer"
- >
- <Link href={`blog/${props.slug}`}>
- <h3>
- <span className="">
- {props.title}
- </span>
- </h3>
- </Link>
- <div className="mb-0 text-zinc-400 flex flex-wrap justify-between">
- <span>
- {props.subtitle}
- </span>
- <span>
- {props.date}
- </span>
- </div>
- </div>
- );
+ return (
+ <div
+ key={props.slug}
+ className="mb-0 text-zinc-400 px-2 py-1 transition ease-in-out rounded-lg hover:dark:bg-[#313131] hover:bg-primary-100 hover:-translate-y-1 cursor-pointer"
+ >
+ <Link href={`blog/${props.slug}`}>
+ <h3>
+ <span className="">{props.title}</span>
+ </h3>
+ </Link>
+ <div className="mb-0 text-zinc-400 flex flex-wrap justify-between">
+ <span>{props.subtitle}</span>
+ <span>{props.date}</span>
+ </div>
+ </div>
+ );
};
-export default PostPreview;
\ No newline at end of file
+export default PostPreview;
And I see that biome was added to my packages and lock files
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git diff pnpm-lock.yaml
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5f334e7..44350da 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -60,6 +60,11 @@ dependencies:
specifier: 5.2.2
version: 5.2.2
+devDependencies:
+ '@biomejs/biome':
+ specifier: 1.1.2
+ version: 1.1.2
+
packages:
/@aashutoshrathi/word-wrap@1.2.6:
@@ -79,6 +84,74 @@ packages:
regenerator-runtime: 0.14.0
dev: false
+ /@biomejs/biome@1.1.2:
+ resolution: {integrity: sha512-JEVWchqo0Xhl86IJgOh0xESWnNRUXBUDByCBR8TA4lIPzm/6U6Tv77+MblNkZ8MvwCtP6PlBNGdQcGKKabtuHA==}
+ engines: {node: '>=14.*'}
+ hasBin: true
+ requiresBuild: true
+ optionalDependencies:
+ '@biomejs/cli-darwin-arm64': 1.1.2
+ '@biomejs/cli-darwin-x64': 1.1.2
+ '@biomejs/cli-linux-arm64': 1.1.2
+ '@biomejs/cli-linux-x64': 1.1.2
+ '@biomejs/cli-win32-arm64': 1.1.2
+ '@biomejs/cli-win32-x64': 1.1.2
+ dev: true
+
+ /@biomejs/cli-darwin-arm64@1.1.2:
+ resolution: {integrity: sha512-YyqWeNZchPxlvxtdo2vMBkzrwllaNS3+DZ6j01mUCVIZE9kAzF/edMV2O38L2AEtnRLU1TI1f71Jai3ThILClg==}
+ engines: {node: '>=14.*'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
...snip...
At this point, it’s worth firing up a dev instance just to be sure we didn’t break anything:
I’ll now add to a commit and push to a new branch.
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git add src/
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git add package.json
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git add pnpm-lock.yaml
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git add biome.json
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git commit -m "First Linter Updates"
[linter 5affa2a] First Linter Updates
19 files changed, 570 insertions(+), 491 deletions(-)
create mode 100644 biome.json
rewrite src/app/blog/[slug]/page.tsx (74%)
rewrite src/app/blog/page.tsx (69%)
rewrite src/app/components/Navbar.tsx (88%)
rewrite src/app/components/common/Spinner.tsx (82%)
rewrite src/app/components/preview/PostPreview.tsx (82%)
rewrite src/app/fun/page.tsx (69%)
rewrite src/app/guestbook/page.tsx (69%)
rewrite src/app/layout.tsx (84%)
rewrite src/app/libs/Metadata/getPostMetadata.ts (79%)
rewrite src/app/page.tsx (99%)
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git push
fatal: The current branch linter has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin linter
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ git push --set-upstream origin linter
Enumerating objects: 66, done.
Counting objects: 100% (66/66), done.
Delta compression using up to 16 threads
Compressing objects: 100% (26/26), done.
Writing objects: 100% (34/34), 5.63 KiB | 2.81 MiB/s, done.
Total 34 (delta 18), reused 0 (delta 0)
remote: Resolving deltas: 100% (18/18), completed with 17 local objects.
remote:
remote: Create a pull request for 'linter' on GitHub by visiting:
remote: https://github.com/idjohnson/nodesite/pull/new/linter
remote:
To https://github.com/idjohnson/nodesite.git
* [new branch] linter -> linter
Branch 'linter' set up to track remote branch 'linter' from 'origin'.
I created a PR - this is also to ensure we can build an image without issue (that Docker is also fine).
Biome Ignores
Say that Biome flags something you know should be the way it is.
For instance, I have a generic page.tsx that is the basis for any blog entry. Biome said it was in poor form to allow any props:
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm biome check ./src/
...snip...
./src/app/blog/[slug]/page.tsx:24:26 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Unexpected any. Specify a different type.
22 │ };
23 │
> 24 │ const PostPage = (props: any) => {
│ ^^^
25 │ const slug = props.params.slug;
26 │ const post = getPostContent(slug);
ℹ any disables many type checking rules. Its use should be avoided.
The number of diagnostics exceeds the number allowed by Biome.
Diagnostics not shown: 13.
Checked 16 file(s) in 5ms
Found 25 error(s)
check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Some errors were emitted while running checks.
I then added a “biome-ignore” comment to skip this file with an explanation. (note: this didn’t always work to stop the check)
const PostPage =
// biome-ignore format: This is a generic page for blogs that could have a growing variety of props. I would not be ideal to restrict to just specific props.
(props: any) => {
const slug = props.params.slug;
const post = getPostContent(slug);
const tempDate = new Date(post.data.date);
const date = tempDate.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
and now it skips that line, albeit not that file. It still flagged my out of order imports:
./src/app/blog/[slug]/page.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ℹ Import statements could be sorted:
1 1 │ import fs from "fs";
2 │ - import·matter·from·"gray-matter";
2 │ + import·Tag·from·"@/app/components/common/Tag";
3 │ + import·getPostMetadata·from·"@/app/libs/Metadata/getPostMetadata";
4 │ + import·matter·from·"gray-matter";
3 5 │ import Markdown from "markdown-to-jsx";
4 │ - import·getPostMetadata·from·"@/app/libs/Metadata/getPostMetadata";
5 │ - import·Link·from·"next/link";
6 │ - import·{·AiOutlineCaretLeft·}·from·"react-icons/ai";
7 │ - import·Tag·from·"@/app/components/common/Tag";
6 │ + import·Link·from·"next/link";
7 │ + import·{·AiOutlineCaretLeft·}·from·"react-icons/ai";
8 8 │
9 9 │ const getPostContent = (slug: string) => {
./src/app/components/Navbar.tsx organizeImports ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ℹ Import statements could be sorted:
2 2 │
3 3 │ import { motion } from "framer-motion";
4 │ - import·{·useState·}·from·"react";
4 │ + import·Link·from·"next/link";
5 5 │ import { usePathname } from "next/navigation";
6 │ - import·Link·from·"next/link";
6 │ + import·{·useState·}·from·"react";
7 7 │
8 8 │ const navItems = [
The number of diagnostics exceeds the number allowed by Biome.
Diagnostics not shown: 14.
Checked 16 file(s) in 6ms
Found 25 error(s)
check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Some errors were emitted while running checks.
In the end, I found I really didn’t want to figure out how to make exceptions for every any
used. This was fine to me.
We can see some of the catches:
./src/app/libs/Metadata/getPostMetadata.ts:18:41 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━
✖ Unexpected any. Specify a different type.
16 │ subtitle: matterResult.data.subtitle,
17 │ slug: fileName.replace(".md", ""),
> 18 │ tags: matterResult.data.tags.map((e: any) => e),
│ ^^^
19 │ };
20 │ });
ℹ any disables many type checking rules. Its use should be avoided.
./src/app/libs/Metadata/getPostMetadata.ts lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ The file contains diagnostics that needs to be addressed.
./src/app/blog/[slug]/page.tsx:24:17 suppressions/parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ failed to parse category "noExplicitAny"
22 │ };
23 │
> 24 │ // biome-ignore noExplicitAny: This is a generic page for blogs that could have a growing variety of props. I would not be ideal to restrict to just specific props.
│ ^^^^^^^^^^^^^
25 │ const PostPage = (props: any) => {
26 │ const slug = props.params.slug;
./src/app/blog/[slug]/page.tsx:25:26 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ Unexpected any. Specify a different type.
It took me a bit to figure out this next part as there doesn’t exist documentation to explain.
That error: “lint/suspicious/noExplicitAny” can be skipped by adding a “off” block in our rules section of the biome.json by adding
"suspicious": {
"noExplicitAny": "off"
}
This made the Biome config file now look as such:
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ cat biome.json
{
"$schema": "https://biomejs.dev/schemas/1.1.2/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "off"
}
}
}
}
With all the other corrections applied:
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm biome check ./src/
Checked 16 file(s) in 3ms
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ pnpm biome ci ./src/
Checked 16 file(s) in 2ms
YAY! we did it…
But ideally, we would always check. Even better if in PRs.
Adding to our CI checks
I really wanted to add the lint in the Dockerfile
I even tried putting it into npm and running that way
FROM node:16.14.2-alpine AS BUILD_IMAGE
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm
# install dependencies
RUN pnpm install
COPY . .
# ci check
COPY biome.json ./
RUN npm run biomecheck
# build
RUN pnpm build
# remove dev dependencies
RUN pnpm prune --production
FROM node:16.14.2-alpine
WORKDIR /app
RUN npm install -g pnpm
# copy from build image
COPY --from=BUILD_IMAGE /app/package.json ./package.json
COPY --from=BUILD_IMAGE /app/node_modules ./node_modules
COPY --from=BUILD_IMAGE /app/.next ./.next
COPY --from=BUILD_IMAGE /app/public ./public
EXPOSE 3000
# ENTRYPOINT ["tail", "-f", "/dev/null"]
CMD ["pnpm", "start"]
But it always errors on an ENOENT error
#14 [build_image 8/10] RUN npm run biomecheck
#14 0.486
#14 0.486 > freshbrewed-app@0.1.0 biomecheck
#14 0.486 > pnpm biome check ./src/
#14 0.486
#14 1.209 /app/node_modules/.pnpm/@biomejs+biome@1.1.2/node_modules/@biomejs/biome/bin/biome:40
#14 1.209 throw result.error;
#14 1.209 ^
#14 1.209
#14 1.209 Error: spawnSync /app/node_modules/.pnpm/@biomejs+cli-linux-x64@1.1.2/node_modules/@biomejs/cli-linux-x64/biome ENOENT
#14 1.209 at Object.spawnSync (node:internal/child_process:1111:20)
#14 1.209 at Object.spawnSync (node:child_process:814:24)
#14 1.209 at Object.<anonymous> (/app/node_modules/.pnpm/@biomejs+biome@1.1.2/node_modules/@biomejs/biome/bin/biome:22:42)
#14 1.209 at Module._compile (node:internal/modules/cjs/loader:1103:14)
#14 1.209 at Object.Module._extensions..js (node:internal/modules/cjs/loader:1157:10)
#14 1.209 at Module.load (node:internal/modules/cjs/loader:981:32)
#14 1.209 at Function.Module._load (node:internal/modules/cjs/loader:822:12)
#14 1.209 at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
#14 1.209 at node:internal/main/run_main_module:17:47 {
#14 1.209 errno: -2,
#14 1.209 code: 'ENOENT',
#14 1.209 syscall: 'spawnSync /app/node_modules/.pnpm/@biomejs+cli-linux-x64@1.1.2/node_modules/@biomejs/cli-linux-x64/biome',
#14 1.209 path: '/app/node_modules/.pnpm/@biomejs+cli-linux-x64@1.1.2/node_modules/@biomejs/cli-linux-x64/biome',
#14 1.209 spawnargs: [ 'check', './src/' ]
#14 1.209 }
#14 ERROR: process "/bin/sh -c npm run biomecheck" did not complete successfully: exit code: 1
------
> [build_image 8/10] RUN npm run biomecheck:
1.209 at Module.load (node:internal/modules/cjs/loader:981:32)
1.209 at Function.Module._load (node:internal/modules/cjs/loader:822:12)
1.209 at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
1.209 at node:internal/main/run_main_module:17:47 {
1.209 errno: -2,
1.209 code: 'ENOENT',
1.209 syscall: 'spawnSync /app/node_modules/.pnpm/@biomejs+cli-linux-x64@1.1.2/node_modules/@biomejs/cli-linux-x64/biome',
1.209 path: '/app/node_modules/.pnpm/@biomejs+cli-linux-x64@1.1.2/node_modules/@biomejs/cli-linux-x64/biome',
1.209 spawnargs: [ 'check', './src/' ]
1.209 }
------
WARNING: No output specified with docker-container driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load
Dockerfile:13
--------------------
11 | # ci check
12 | COPY biome.json ./
13 | >>> RUN npm run biomecheck
14 |
15 | # build
--------------------
ERROR: failed to solve: process "/bin/sh -c npm run biomecheck" did not complete successfully: exit code: 1
Error: buildx failed with: ERROR: failed to solve: process "/bin/sh -c npm run biomecheck" did not complete successfully: exit code: 1
However, I was able to add it as a standard GH action step.
I needed to add NodeJS 16, then install npm and run lint (eslint) and biomecheck (biome)
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
- run: npm install
- run: npm run lint
- run: npm run biomecheck
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1
with:
cosign-release: 'v2.1.1'
Though this left a lot of junk locally that broke docker in later steps
I broke into two jobs to address that:
jobs:
ciCheck:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
- run: npm install
- run: npm run lint
- run: npm run biomecheck
build:
needs: ciCheck
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
... snip ..
We can now see the flow in action
This prompted me to set it to NOT deploy PR builds since they aren’t actually pushed to GHCR anyways
- name: SetupDemoKConfig
if: ${{ github.event_name != 'pull_request' }}
run: |
echo ${{ secrets.KCONFIGTEST }} | base64 --decode > /tmp/kconfig
kubectl get nodes --insecure-skip-tls-verify --kubeconfig=/tmp/kconfig
# Deploy
kubectl delete -f ./deploy/k8sRSGH.yaml --insecure-skip-tls-verify --kubeconfig=/tmp/kconfig || true
kubectl apply -f ./deploy/k8sRSGH.yaml --insecure-skip-tls-verify --kubeconfig=/tmp/kconfig
Now it skipped that step on the PR
Let’s merge and see what happens. It should build, push and host on the cluster
Let’s rebuild to just to see the deploy update at that step
TLS Ingress
Let’s assume we want to host in our Production cluster.
I’ll first want to create an A Record to route here.
I’ll create and apply an A record
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ cat r53-nodesite.json
{
"Comment": "CREATE nodesite fb.s A record ",
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "nodesite.freshbrewed.science",
"Type": "A",
"TTL": 300,
"ResourceRecords": [
{
"Value": "75.73.224.240"
}
]
}
}
]
}
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ aws route53 change-resource-record-sets --hosted-zone-id Z39E8QFU0F9PZP --change-batch file://r53-nodesite.json
{
"ChangeInfo": {
"Id": "/change/C084834118M5X7DOE7IAO",
"Status": "PENDING",
"SubmittedAt": "2023-09-15T11:16:40.835Z",
"Comment": "CREATE nodesite fb.s A record "
}
}
For the ingress, we need to remind ourselves of the deployment yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: fbnextgh-deployment
spec:
replicas: 3 # You can adjust the number of replicas as needed
selector:
matchLabels:
app: fbnext-app
template:
metadata:
labels:
app: fbnext-app
spec:
containers:
- name: fbnext-container
image: ghcr.io/idjohnson/nodesite:main
ports:
- containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: fbnextgh-service
spec:
selector:
app: fbnext-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP
While the Deployment and ReplicaSet use “fbnext-app” on 3000, the service is “fbnextgh-service” using the standard HTTP port.
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ cat ingress-nodesite.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.org/websocket-services: nodesite-external-ip
labels:
app.kubernetes.io/instance: nodesiteingress
name: nodesiteingress
spec:
rules:
- host: nodesite.freshbrewed.science
http:
paths:
- backend:
service:
name: fbnextgh-service
port:
number: 80
path: /
pathType: ImplementationSpecific
tls:
- hosts:
- nodesite.freshbrewed.science
secretName: nodesite-tls
builder@DESKTOP-QADGF36:~/Workspaces/nodesite/freshbrewed-app$ kubectl apply -f ingress-nodesite.yaml
ingress.networking.k8s.io/nodesiteingress created
The last bit of fun is to swap out the kubeconfig for the primary cluster. None of this will do anything if we aren’t deploying into the exposed cluster!
I updated the Kubeconfig as a base64 value in the secrets
This likely will something to consider replacing ith a private agent runner. That is, I have automations to deal with the occasional changing of my external IP, but this will have the fixed IP.
I could make one depend on the other. Something like:
$ export DETIPADD=`host -4 nodesite.freshbrewed.science | tail -n1 | sed 's/.* //' | tr -d '\n'`
$ sed -i "s/server: https:\/\/[^:]*:/server: https:\/\/$DETIPADD:/" /tmp/kconfig
I updated the step in a new branch
- name: SetupDemoKConfig
if: ${{ github.event_name != 'pull_request' }}
run: |
set +x
echo ${{ secrets.KCONFIGTEST }} | base64 --decode > /tmp/kconfig
set -x
# Autocorrect IP
export DETIPADD=`host -4 nodesite.freshbrewed.science | tail -n1 | sed 's/.* //' | tr -d '\n'`
sed -i "s/server: https:\/\/[^:]*:/server: https:\/\/$DETIPADD:/" /tmp/kconfig
# Test
kubectl get nodes --insecure-skip-tls-verify --kubeconfig=/tmp/kconfig
# Deploy
kubectl delete -f ./deploy/k8sRSGH.yaml --insecure-skip-tls-verify --kubeconfig=/tmp/kconfig || true
kubectl apply -f ./deploy/k8sRSGH.yaml --insecure-skip-tls-verify --kubeconfig=/tmp/kconfig
I wanted to circle back on tools. I was using KDash we spoke on last week and noticed something:
That errant websocket entry was breaking the Ingress and I hadn’t caught on yet.
Fixing gives a 502 Bad Gateway which is what I expect when there isn’t a matching service (yet).
Which brings us to the final merge push and deploy where you can see the end to end flow.
I used k9s above as KDash doesn’t have a search/filter feature. The sample site is now live.
Summary
We covered a lot today. We got linting going with Biome and used it to cleanup a lot of code. We figured out how to add specific skips then moved on to CICD where we added it to the build. Trying to use the Dockerfile proved unsuccessful but just adding build steps worked. We split the build into multiple stages and lastly, worked out proper PR vs Main pipeline steps and a production build to a routable TLS endpoint.
At this point, if one was to update or create blog entry and push the PR, it would show up live to nodesite.freshbrewed.science.