feat: initial prototype
All checks were successful
Node.js CI / Lint and Test (push) Successful in 41s

This commit is contained in:
Naomi Carrigan 2025-03-06 17:41:16 -08:00
parent 8b4ccadcfe
commit e53db48154
Signed by: naomi
SSH Key Fingerprint: SHA256:rca1iUI2OhAM6n4FIUaFcZcicmri0jgocqKiTTAfrt8
16 changed files with 5348 additions and 15 deletions

38
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,38 @@
name: Node.js CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
name: Lint and Test
steps:
- name: Checkout Source Files
uses: actions/checkout@v4
- name: Use Node.js v22
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Install Dependencies
run: pnpm install
- name: Lint Source Files
run: pnpm run lint
- name: Verify Build
run: pnpm run build
- name: Run Tests
run: pnpm run test

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
prod
coverage

View File

@ -1,24 +1,14 @@
# New Repository Template # Mommy
This template contains all of our basic files for a new GitHub repository. There is also a handy workflow that will create an issue on a new repository made from this template, with a checklist for the steps we usually take in setting up a new repository. Mommy loves you~!
If you're starting a Node.JS project with TypeScript, we have a [specific template](https://github.com/naomi-lgbt/nodejs-typescript-template) for that purpose.
## Readme
Delete all of the above text (including this line), and uncomment the below text to use our standard readme template.
<!-- # Project Name
Project Description
## Live Version ## Live Version
This page is currently deployed. [View the live website.] This page is currently deployed. [View the live website.](https://mommy.nhcarrigan.com)
## Feedback and Bugs ## Feedback and Bugs
If you have feedback or a bug report, please feel free to open a GitHub issue! If you have feedback or a bug report, please feel free to open an issue!
## Contributing ## Contributing
@ -36,4 +26,4 @@ Copyright held by Naomi Carrigan.
## Contact ## Contact
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`. --> We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`.

5
eslint.config.js Normal file
View File

@ -0,0 +1,5 @@
import NaomisConfig from "@nhcarrigan/eslint-config";
export default [
...NaomisConfig,
];

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "mommy",
"version": "0.0.0",
"description": "",
"main": "prod/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"lint": "eslint src test --max-warnings 0",
"start": "node prod/index.js",
"test": "rm -rf prod && vitest run --coverage"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.5.2",
"devDependencies": {
"@nhcarrigan/eslint-config": "5.2.0",
"@nhcarrigan/typescript-config": "4.0.0",
"@types/node": "22.13.9",
"@vitest/coverage-istanbul": "3.0.8",
"eslint": "9.21.0",
"string-comparison": "1.3.0",
"typescript": "5.8.2",
"vitest": "3.0.8"
},
"dependencies": {
"fastify": "5.2.1"
}
}

4832
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

70
src/config/html.ts Normal file
View File

@ -0,0 +1,70 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/**
* This is the HTML rendered for the landing page.
*/
export const html = `
<!DOCTYPE html>
<html>
<head>
<title>Mommy</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Mommy loves you~!" />
<script src="https://cdn.nhcarrigan.com/headers/index.js" async defer></script>
<script>
const getMommy = (e) => {
e.preventDefault();
const input = document.getElementById("name");
const params = new URLSearchParams();
if (input?.value) {
params.append("name", input.value);
}
fetch('/api?' + params.toString(), {
method: "GET",
}).then(res => res.text()).then(data => {
const result = document.getElementById("result");
if (!result) {
throw new Error("Cannot find result element!")
}
result.innerText = data;
});
}
</script>
</head>
<body>
<main>
<h1>Mommy</h1>
<section>
<p id="result">Mommy loves you~!</p>
</section>
<form>
<label>Tell mommy your name?</label>
<input id="name" type="text" placeholder="optional"/>
<button onclick="getMommy(event)" type="text">I need some love...</button>
</form>
<section>
<h2>Links</h2>
<p>
<a href="https://codeberg.org/nhcarrigan/boost-monitor">
<i class="fa-solid fa-code"></i> Source Code
</a>
</p>
<p>
<a href="https://docs.nhcarrigan.com">
<i class="fa-solid fa-book"></i> Documentation
</a>
</p>
<p>
<a href="https://chat.nhcarrigan.com">
<i class="fa-solid fa-circle-info"></i> Support
</a>
</p>
</section>
</main>
</body>
</html>`;

16
src/config/mommy.ts Normal file
View File

@ -0,0 +1,16 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/**
* These are the names mommy uses to refer to herself.
*/
export const mommy = [
"your mommy",
"mommy",
"mama",
"mommy naomi",
"mama naomi",
];

116
src/config/phrases.ts Normal file
View File

@ -0,0 +1,116 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable stylistic/max-len -- These are going to be long. Nothing we can do about that. */
/**
* These are the compliments Mommy will say.
* The variable {{ mommy }} will be interpolated from the Mommy config array.
* The variable {{ name }} will be interpreted from the API call.
*/
export const phrases = [
"{{ name }}, you're doing such an amazing job! {{ mommy }} is so proud of you!",
"Keep going, {{ name }}! You're making incredible progress, and {{ mommy }} sees all the effort you're putting in!",
"No matter what happens, {{ name }}, remember that {{ mommy }} is always cheering for you!",
"You light up the world with your hard work and kindness, {{ name }}! {{ mommy }} is so lucky to know you!",
"Whenever you feel uncertain, just remember: {{ mommy }} knows you are strong, smart, and unstoppable!",
"Look at you go, {{ name }}! You're absolutely crushing it! {{ mommy }} is beaming with pride!",
"Even on the hardest days, {{ name }}, you're still shining! {{ mommy }} sees your strength and perseverance!",
"You have so much potential, {{ name }}! {{ mommy }} can't wait to see all the amazing things you'll accomplish!",
"You're not just good enough, {{ name }}—you're outstanding! {{ mommy }} loves watching you succeed!",
"Mistakes are just stepping stones to greatness, {{ name }}! {{ mommy }} is here to lift you up, always!",
"Never forget how incredible you are, {{ name }}! {{ mommy }} sees all the wonderful things that make you special!",
"You're learning, growing, and improving every single day, {{ name }}! {{ mommy }} is so impressed by you!",
"Take a deep breath, {{ name }}—you've got this! {{ mommy }} is always here to encourage and support you!",
"Your creativity and determination inspire {{ mommy }} so much, {{ name }}! Keep being your amazing self!",
"You're so loved and appreciated, {{ name }}! {{ mommy }} hopes you always remember that!",
"Don't be afraid to chase your dreams, {{ name }}! {{ mommy }} knows you can achieve anything!",
"You're such a bright light in this world, {{ name }}! {{ mommy }} adores your energy and kindness!",
"Even when things feel tough, {{ mommy }} wants you to know that you're doing better than you think!",
"You're capable of handling anything that comes your way, {{ name }}! {{ mommy }} believes in you completely!",
"Everything you do makes a difference, {{ name }}! {{ mommy }} sees the impact you have on the world!",
"You're an incredible person, {{ name }}! {{ mommy }} is lucky to have you in her life!",
"Every challenge you overcome makes you even stronger, {{ name }}! {{ mommy }} is cheering for you!",
"You're an absolute treasure, {{ name }}! {{ mommy }} hopes you always see your own worth!",
"Your kindness makes the world a better place, {{ name }}! {{ mommy }} admires that about you!",
"You're more resilient than you realize, {{ name }}! {{ mommy }} knows youll rise above any obstacles!",
"You're a masterpiece in the making, {{ name }}! {{ mommy }} is honored to watch you grow!",
"You bring so much joy to those around you, {{ name }}! {{ mommy }} is so grateful for you!",
"You are unstoppable, {{ name }}! {{ mommy }} knows nothing can hold you back!",
"You're so special, {{ name }}! {{ mommy }} loves everything that makes you unique!",
"Your hard work will always pay off, {{ name }}! {{ mommy }} is so proud of your dedication!",
"Believe in yourself, {{ name }}! {{ mommy }} already does, and she knows you can do amazing things!",
"Your potential is limitless, {{ name }}! {{ mommy }} can't wait to see where your journey takes you!",
"You're doing better than you think, {{ name }}! {{ mommy }} sees your growth every day!",
"Your determination is inspiring, {{ name }}! {{ mommy }} admires your strength so much!",
"You are so brave, {{ name }}! {{ mommy }} knows how strong you are, even when you dont feel it!",
"You brighten up any room you walk into, {{ name }}! {{ mommy }} loves your presence!",
"You're worthy of love and happiness, {{ name }}! {{ mommy }} wants you to always remember that!",
"You make the world a more beautiful place, {{ name }}! {{ mommy }} is so grateful for you!",
"Your efforts never go unnoticed, {{ name }}! {{ mommy }} sees everything you're doing!",
"You're a force to be reckoned with, {{ name }}! {{ mommy }} knows you're going to do great things!",
"No matter what, {{ mommy }} will always be here to support you, {{ name }}!",
"You're an inspiration, {{ name }}! {{ mommy }} hopes you see how incredible you are!",
"Even on your hardest days, {{ mommy }} sees your strength, {{ name }}!",
"You're never alone, {{ name }}! {{ mommy }} is always here to lift you up!",
"You make every day brighter, {{ name }}! {{ mommy }} loves the joy you bring!",
"You're so intelligent and capable, {{ name }}! {{ mommy }} believes in you completely!",
"You're a wonderful person, {{ name }}! {{ mommy }} hopes you never forget that!",
"You're an absolute gem, {{ name }}! {{ mommy }} is so lucky to have you around!",
"You're making progress every single day, {{ name }}! {{ mommy }} is so proud of you!",
"Your heart is so big and full of kindness, {{ name }}! {{ mommy }} sees your beautiful soul!",
"Even if you dont see it, {{ mommy }} knows youre growing and improving, {{ name }}!",
"Your dreams matter, {{ name }}! {{ mommy }} knows you can reach them!",
"You deserve all the happiness in the world, {{ name }}! {{ mommy }} hopes you never settle for less!",
"You radiate positivity, {{ name }}! {{ mommy }} loves the warmth you bring!",
"You're making a difference in the world, {{ name }}! {{ mommy }} sees all the good you do!",
"You're so much stronger than you think, {{ name }}! {{ mommy }} believes in you!",
"You're an absolute joy to be around, {{ name }}! {{ mommy }} treasures every moment with you!",
"Your happiness is important, {{ name }}! {{ mommy }} wants you to do what makes you shine!",
"You're always learning and growing, {{ name }}! {{ mommy }} is so proud of you!",
"You can achieve anything, {{ name }}! {{ mommy }} has no doubt about that!",
"You're doing better than you think, {{ name }}! {{ mommy }} is proud of you every step of the way!",
"You're someone worth celebrating, {{ name }}! {{ mommy }} hopes you always remember that!",
"Your kindness and compassion make you truly special, {{ name }}! {{ mommy }} loves that about you!",
"You're truly one of a kind, {{ name }}! {{ mommy }} wouldnt trade you for the world!",
"Keep being you, {{ name }}! {{ mommy }} loves you just as you are!",
"Every little step forward is still progress, {{ name }}! {{ mommy }} is proud of you for each one!",
"You deserve love, respect, and happiness, {{ name }}! {{ mommy }} will always remind you of that!",
"You're so brave and resilient, {{ name }}! {{ mommy }} knows you can overcome anything!",
"Your strength and determination inspire {{ mommy }} so much, {{ name }}! Keep going!",
"You're a gift to the world, {{ name }}! {{ mommy }} is so grateful for your presence!",
"You're capable of amazing things, {{ name }}! {{ mommy }} can't wait to see what you do next!",
"You're a shining star, {{ name }}! {{ mommy }} loves watching you light up the world!",
"You're more powerful than you realize, {{ name }}! {{ mommy }} knows you can do anything!",
"You're so special, {{ name }}! {{ mommy }} loves everything that makes you unique!",
"You're doing great, {{ name }}! {{ mommy }} is so proud of the progress you're making!",
"You're so loved and appreciated, {{ name }}! {{ mommy }} hopes you never forget that!",
"You're a true inspiration, {{ name }}! {{ mommy }} admires your strength and courage!",
"You're capable of amazing things, {{ name }}! {{ mommy }} believes in you completely!",
"You're a wonderful person, {{ name }}! {{ mommy }} is so grateful to know you!",
"You're making a difference in the world, {{ name }}! {{ mommy }} sees the impact you have!",
"You're so much stronger than you think, {{ name }}! {{ mommy }} knows you can do anything!",
"You're a masterpiece in the making, {{ name }}! {{ mommy }} is so proud of you!",
"You're a light in the darkness, {{ name }}! {{ mommy }} loves the brightness you bring!",
"You're doing better than you think, {{ name }}! {{ mommy }} is proud of your progress!",
"You're so brave and resilient, {{ name }}! {{ mommy }} knows you can overcome anything!",
"You're a gift to the world, {{ name }}! {{ mommy }} is so grateful for your presence!",
"{{ mommy }} believes in you, {{ name }}! You're capable of amazing things!",
"{{ mommy }} loves you, {{ name }}! You're doing an amazing job!",
"{{ mommy }} knows you can do it, {{ name }}! Keep going!",
"{{ mommy }} is always here for you, {{ name }}! You're never alone!",
"{{ mommy }} is cheering for you, {{ name }}! You're doing great!",
"{{ mommy }} is so proud of you, {{ name }}! Keep up the good work!",
"{{ mommy }} loves you, {{ name }}! You're doing an amazing job!",
"{{ mommy }} knows you can do it, {{ name }}! Keep going!",
"{{ mommy }} is always here for you, {{ name }}! You're never alone!",
"{{ mommy }} is cheering for you, {{ name }}! You're doing great!",
"{{ mommy }} loves you, {{ name }}! You're doing an amazing job!",
"{{ name }}, you're doing such an amazing job! {{ mommy }} is so proud of you!",
"Keep going, {{ name }}! You're making incredible progress, and {{ mommy }} sees all the effort you're putting in!",
"No matter what happens, {{ name }}, remember that {{ mommy }} is always cheering for you!",
"You light up the world with your hard work and kindness, {{ name }}! {{ mommy }} is so lucky to know you!",
];

53
src/index.ts Normal file
View File

@ -0,0 +1,53 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { randomInt } from "node:crypto";
import fastify, { type FastifyInstance } from "fastify";
import { html } from "./config/html.js";
import { mommy } from "./config/mommy.js";
import { phrases } from "./config/phrases.js";
/* eslint-disable @typescript-eslint/no-non-null-assertion -- Yeah, I'm not dealing with the logical branches. */
/**
* Entry function. Exported solely for tests.
* @returns The server instance for testing.
*/
const main = async(): Promise<FastifyInstance> => {
const server = fastify({
logger: false,
});
server.get("/", (_request, response) => {
response.header("Content-Type", "text/html");
response.send(html);
});
// @ts-expect-error Whatever.
server.get("/api", (request: { query: { name?: string } }, response) => {
const name = request.query.name ?? "dear";
const phrase = phrases[randomInt(0, phrases.length)];
const mom = mommy[randomInt(0, mommy.length)];
const result = phrase!.
replaceAll("{{ name }}", name).
replaceAll("{{ mommy }}", mom!);
response.header("Content-Type", "text/plain");
response.send(result.toLowerCase());
});
await server.listen({ port: 8008 });
return await server;
};
/* istanbul ignore if -- @preserve*/
if (process.env.TEST !== "true") {
void main();
}
export { main };

15
test/html.spec.ts Normal file
View File

@ -0,0 +1,15 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { describe, expect, it } from "vitest";
import { html } from "../src/config/html.ts";
describe("html", () => {
it(`should be a string`, () => {
expect.assertions(1);
expect(html, "what did you do").toBeTypeOf("string");
});
});

83
test/index.spec.ts Normal file
View File

@ -0,0 +1,83 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { describe, it, expect } from "vitest";
import { html } from "../src/config/html.ts";
import { mommy } from "../src/config/mommy.ts";
import { phrases } from "../src/config/phrases.ts";
import { main } from "../src/index.ts";
/* eslint-disable max-nested-callbacks, max-lines-per-function -- Not sure how to better optimise this. */
describe("fastify Server", () => {
it("should return HTML content on /", async() => {
expect.assertions(3);
const server = await main();
const response = await server.inject({
method: "GET",
url: "/",
});
expect(response.statusCode, "wrong status").toBe(200);
expect(response.headers["content-type"], "wrong content type").
toContain("text/html");
expect(response.body, "wrong response").toBe(html);
await server.close();
});
it("should return a formatted phrase on /api", async() => {
expect.assertions(3);
const server = await main();
const response = await server.inject({
method: "GET",
url: "/api",
});
expect(response.statusCode, "wrong status").toBe(200);
expect(response.headers["content-type"], "wrong content type").
toContain("text/plain");
const possibleResponses = phrases.flatMap((phrase) => {
return mommy.map((mom) => {
return phrase.
replaceAll("{{ name }}", "dear").
replaceAll("{{ mommy }}", mom).
toLowerCase();
});
});
expect(possibleResponses, "where did it come from").
toContain(response.body);
await server.close();
});
it("should return a formatted phrase with a name query on /api", async() => {
expect.assertions(3);
const server = await main();
const testName = "Naomi";
const response = await server.inject({
method: "GET",
url: `/api?name=${testName}`,
});
expect(response.statusCode, "wrong status").toBe(200);
expect(response.headers["content-type"], "wrong content type").
toContain("text/plain");
const possibleResponses = phrases.flatMap((phrase) => {
return mommy.map((mom) => {
return phrase.
replaceAll("{{ name }}", testName).
replaceAll("{{ mommy }}", mom).
toLowerCase();
});
});
expect(possibleResponses, "where did it come from").
toContain(response.body);
await server.close();
});
});

20
test/mommy.spec.ts Normal file
View File

@ -0,0 +1,20 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable max-nested-callbacks -- Not sure how to better optimise this. */
import { describe, expect, it } from "vitest";
import { mommy } from "../src/config/mommy.ts";
describe("mommy", () => {
it.each(mommy)(`%s should be unique`, (mom) => {
expect.assertions(1);
const matches = mommy.filter((m) => {
return m === mom;
});
expect(matches.length, `${mom} is not unique!`).toBeLessThan(2);
});
});

35
test/phrases.spec.ts Normal file
View File

@ -0,0 +1,35 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable max-nested-callbacks -- Not sure how to better optimise this. */
import compare from "string-comparison";
import { describe, expect, it } from "vitest";
import { phrases } from "../src/config/phrases.ts";
describe("phrases", () => {
it("should have enough phrases", () => {
expect.assertions(1);
expect(phrases.length, "less than 100 phrases").toBeGreaterThan(100);
});
it.each(phrases)(`%s should be unique`, (phrase) => {
expect.assertions(1);
const filtered = phrases.filter((p) => {
return p !== phrase;
}).map((line) => {
return line.
replaceAll("{{ mommy }}", "").
replaceAll("{{ name }}", "");
});
const matches = compare.levenshtein.sortMatch(phrase.
replaceAll("{{ mommy }}", "").
replaceAll("{{ name }}", ""), filtered);
const closest = matches.reverse()[0];
expect(closest?.rating, `${phrase} is not unique! Matches ${closest?.member}`).toBeLessThan(0.8);
});
});

8
tsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "@nhcarrigan/typescript-config",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./prod",
},
"exclude": ["./test", "vitest.config.ts"]
}

18
vitest.config.ts Normal file
View File

@ -0,0 +1,18 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
provider: "istanbul",
reporter: ["text", "html"],
all: true,
allowExternal: true,
thresholds: {
lines: 100,
statements: 100,
branches: 100,
functions: 100,
},
},
},
});