create-next-pro-cli 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +24 -7
  2. package/dist/bin.bun.js +27 -2
  3. package/dist/bin.node.js +27 -2
  4. package/dist/bin.node.js.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/Projects/default/.env.example +9 -7
  7. package/templates/Projects/default/.github/workflows/quality.yml +40 -9
  8. package/templates/Projects/default/.gitignore.template +33 -15
  9. package/templates/Projects/default/README.md +38 -9
  10. package/templates/Projects/default/bun.lock +120 -134
  11. package/templates/Projects/default/messages/en/_home.json +9 -2
  12. package/templates/Projects/default/messages/en/settings.json +9 -1
  13. package/templates/Projects/default/messages/en/userInfo.json +6 -1
  14. package/templates/Projects/default/messages/fr/_home.json +9 -2
  15. package/templates/Projects/default/messages/fr/settings.json +9 -1
  16. package/templates/Projects/default/messages/fr/userInfo.json +6 -1
  17. package/templates/Projects/default/next.config.ts +11 -0
  18. package/templates/Projects/default/package.json +7 -1
  19. package/templates/Projects/default/playwright.config.ts +29 -6
  20. package/templates/Projects/default/pnpm-workspace.yaml +2 -0
  21. package/templates/Projects/default/scripts/audit.ts +4 -0
  22. package/templates/Projects/default/scripts/package-manager.ts +68 -0
  23. package/templates/Projects/default/src/app/[locale]/(public)/_home/page.tsx +2 -2
  24. package/templates/Projects/default/src/app/[locale]/(public)/layout.tsx +1 -2
  25. package/templates/Projects/default/src/app/[locale]/(public)/login/page.tsx +6 -1
  26. package/templates/Projects/default/src/app/[locale]/(public)/register/page.tsx +6 -1
  27. package/templates/Projects/default/src/app/[locale]/(user)/userInfo/page.tsx +5 -2
  28. package/templates/Projects/default/src/app/[locale]/page.tsx +1 -4
  29. package/templates/Projects/default/src/lib/github/repository.ts +68 -0
  30. package/templates/Projects/default/src/ui/_global/GlobalMain.tsx +10 -5
  31. package/templates/Projects/default/src/ui/_global/ThemeToggle.tsx +18 -8
  32. package/templates/Projects/default/src/ui/_global/UserNav.tsx +5 -1
  33. package/templates/Projects/default/src/ui/_home/GitHubActions.tsx +57 -0
  34. package/templates/Projects/default/src/ui/_home/page-shell.tsx +16 -0
  35. package/templates/Projects/default/src/ui/_home/page-ui.tsx +24 -13
  36. package/templates/Projects/default/src/ui/dashboard/LogoutButton.tsx +1 -1
  37. package/templates/Projects/default/src/ui/dashboard/StatsCard.tsx +2 -2
  38. package/templates/Projects/default/src/ui/dashboard/WelcomeCard.tsx +4 -2
  39. package/templates/Projects/default/src/ui/dashboard/page-ui.tsx +0 -2
  40. package/templates/Projects/default/src/ui/settings/page-ui.tsx +45 -6
  41. package/templates/Projects/default/src/ui/userInfo/page-ui.tsx +54 -7
  42. package/templates/Projects/default/tests/consumer/validate-template.ts +57 -48
  43. package/templates/Projects/default/tests/e2e/template-remediation.playwright.ts +47 -4
  44. package/templates/Projects/default/tests/unit/csp.test.ts +1 -1
  45. package/templates/Projects/default/tests/unit/github-repository.test.ts +45 -0
  46. package/templates/Projects/default/tests/unit/package-manager.test.ts +51 -0
  47. package/templates/Projects/default/tests/unit/template-baseline.test.ts +52 -0
  48. package/templates/Projects/default/tests/unit/user-nav.test.tsx +52 -0
  49. package/templates/Projects/default/tsconfig.json +2 -1
  50. package/templates/Projects/default/vitest.config.ts +10 -0
@@ -2,9 +2,11 @@ import React from "react";
2
2
 
3
3
  export default function WelcomeCard({ t }: { t: (key: string) => string }) {
4
4
  return (
5
- <div className="rounded border bg-white p-6 shadow-sm">
5
+ <div className="rounded border bg-white p-6 shadow-sm dark:bg-black">
6
6
  <h2 className="text-lg font-semibold mb-2">{t("widgets.welcome")}</h2>
7
- <p className="text-gray-600">{t("widgets.description")}</p>
7
+ <p className="text-gray-600 dark:text-gray-300">
8
+ {t("widgets.description")}
9
+ </p>
8
10
  </div>
9
11
  );
10
12
  }
@@ -1,6 +1,5 @@
1
1
  // src/ui/dashboard/page-ui.tsx
2
2
  import { getTranslations } from "next-intl/server";
3
- import BackButton from "@/ui/_global/BackButton";
4
3
  import WelcomeCard from "@/ui/dashboard/WelcomeCard";
5
4
  import StatsCard from "@/ui/dashboard/StatsCard";
6
5
 
@@ -10,7 +9,6 @@ export default async function Dashboard() {
10
9
  <>
11
10
  <div className="flex items-center justify-between mb-6">
12
11
  <h1 className="text-2xl font-bold">{t("title")}</h1>
13
- <BackButton />
14
12
  </div>
15
13
  <p className="mb-8">{t("description")}</p>
16
14
  <section className="grid grid-cols-1 md:grid-cols-2 gap-6">
@@ -1,15 +1,54 @@
1
1
  // src/ui/settings/page-ui.tsx
2
2
  import { getTranslations } from "next-intl/server";
3
+ import LocaleSwitcher from "@/ui/_global/LocaleSwitcher";
4
+ import ThemeToggle from "@/ui/_global/ThemeToggle";
3
5
 
4
6
  export default async function SettingsPageUI() {
5
7
  const t = await getTranslations("settings");
6
8
 
7
9
  return (
8
- <>
9
- <section className="py-8 px-4 max-w-3xl mx-auto">
10
- <h1 className="text-2xl font-bold">{t("title")}</h1>
11
- <p className="mt-2">{t("description")}</p>
12
- </section>
13
- </>
10
+ <section className="py-8 px-4 max-w-3xl mx-auto">
11
+ <h1 className="text-2xl font-bold">{t("title")}</h1>
12
+ <p className="mt-2">{t("description")}</p>
13
+
14
+ <div className="mt-8 grid gap-6">
15
+ <section
16
+ className="rounded border bg-white p-6 shadow-sm dark:bg-black"
17
+ aria-labelledby="language-preference-title"
18
+ >
19
+ <div className="flex items-center justify-between gap-6">
20
+ <div>
21
+ <h2
22
+ id="language-preference-title"
23
+ className="text-lg font-semibold"
24
+ >
25
+ {t("language.title")}
26
+ </h2>
27
+ <p className="mt-1 text-sm text-gray-600 dark:text-gray-300">
28
+ {t("language.description")}
29
+ </p>
30
+ </div>
31
+ <LocaleSwitcher />
32
+ </div>
33
+ </section>
34
+
35
+ <section
36
+ className="rounded border bg-white p-6 shadow-sm dark:bg-black"
37
+ aria-labelledby="theme-preference-title"
38
+ >
39
+ <div className="flex items-center justify-between gap-6">
40
+ <div>
41
+ <h2 id="theme-preference-title" className="text-lg font-semibold">
42
+ {t("theme.title")}
43
+ </h2>
44
+ <p className="mt-1 text-sm text-gray-600 dark:text-gray-300">
45
+ {t("theme.description")}
46
+ </p>
47
+ </div>
48
+ <ThemeToggle />
49
+ </div>
50
+ </section>
51
+ </div>
52
+ </section>
14
53
  );
15
54
  }
@@ -1,15 +1,62 @@
1
1
  // src/ui/userInfo/page-ui.tsx
2
+ import Image from "next/image";
2
3
  import { getTranslations } from "next-intl/server";
3
4
 
4
- export default async function UserInfoPageUI() {
5
+ type UserInfoPageUIProps = {
6
+ user?: {
7
+ name?: string | null;
8
+ email?: string | null;
9
+ image?: string | null;
10
+ };
11
+ };
12
+
13
+ export default async function UserInfoPageUI({ user }: UserInfoPageUIProps) {
5
14
  const t = await getTranslations("userInfo");
6
15
 
7
16
  return (
8
- <>
9
- <section className="py-8 px-4 max-w-3xl mx-auto">
10
- <h1 className="text-2xl font-bold">{t("title")}</h1>
11
- <p className="mt-2">{t("description")}</p>
12
- </section>
13
- </>
17
+ <section className="py-8 px-4 max-w-3xl mx-auto">
18
+ <h1 className="text-2xl font-bold">{t("title")}</h1>
19
+ <p className="mt-2">{t("description")}</p>
20
+
21
+ <div className="mt-8 rounded border bg-white p-6 shadow-sm dark:bg-black">
22
+ <div className="flex flex-col gap-6 sm:flex-row sm:items-center">
23
+ {user?.image ? (
24
+ <Image
25
+ src={user.image}
26
+ alt={t("profile_image_alt")}
27
+ width={96}
28
+ height={96}
29
+ className="h-24 w-24 rounded-full object-cover"
30
+ />
31
+ ) : (
32
+ <div
33
+ className="flex h-24 w-24 items-center justify-center rounded-full bg-muted text-2xl font-semibold text-muted-foreground"
34
+ aria-label={t("profile_image_unavailable")}
35
+ >
36
+ {user?.name?.charAt(0).toUpperCase() || "?"}
37
+ </div>
38
+ )}
39
+
40
+ <dl className="grid flex-1 gap-4">
41
+ <div>
42
+ <dt className="text-sm font-medium text-gray-600 dark:text-gray-300">
43
+ {t("name")}
44
+ </dt>
45
+ <dd className="mt-1 break-words">
46
+ {user?.name || t("unavailable")}
47
+ </dd>
48
+ </div>
49
+ <div>
50
+ <dt className="text-sm font-medium text-gray-600 dark:text-gray-300">
51
+ {t("email")}
52
+ </dt>
53
+ <dd className="mt-1 break-all">
54
+ {user?.email || t("unavailable")}
55
+ </dd>
56
+ </div>
57
+ </dl>
58
+ </div>
59
+ </div>
60
+ </section>
14
61
  );
15
62
  }
@@ -1,66 +1,75 @@
1
- import { spawn } from "node:child_process";
2
- import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
1
+ import { cp, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
3
2
  import { tmpdir } from "node:os";
4
3
  import path from "node:path";
4
+ import {
5
+ installArguments,
6
+ resolvePackageManager,
7
+ runPackageManager,
8
+ } from "../../scripts/package-manager.ts";
5
9
 
6
10
  const root = process.cwd();
7
- const target = await mkdtemp(path.join(tmpdir(), "create-next-pro-template-"));
8
- const manager = process.env.CNP_PACKAGE_MANAGER ?? "bun";
11
+ const target = await mkdtemp(
12
+ path.join(tmpdir(), "create-next-pro-template-consumer-"),
13
+ );
14
+ const manager = resolvePackageManager();
15
+ const gitignoreSource = await stat(path.join(root, ".gitignore.template")).then(
16
+ () => ".gitignore.template",
17
+ () => ".gitignore",
18
+ );
9
19
  const entries = [
10
- ".env.example",
11
- ".gitignore",
12
- ".prettierignore",
13
- "README.md",
14
- "bun.lock",
15
- "eslint.config.mjs",
16
- "messages",
17
- "next-env.d.ts",
18
- "next.config.ts",
19
- "package.json",
20
- "playwright.config.ts",
21
- "pnpm-workspace.yaml",
22
- "postcss.config.mjs",
23
- "public",
24
- "src",
25
- "tailwind.config.ts",
26
- "tests/unit",
27
- "tests/rendering",
28
- "tsconfig.json",
29
- ];
30
-
31
- function run(command: string, args: string[]) {
32
- return new Promise<void>((resolve, reject) => {
33
- const child = spawn(command, args, {
34
- cwd: target,
35
- env: { ...process.env, AUTH_DISABLED: "true", CI: "true" },
36
- stdio: "inherit",
37
- });
38
- child.on("error", reject);
39
- child.on("exit", (code) =>
40
- code === 0
41
- ? resolve()
42
- : reject(new Error(`${command} ${args.join(" ")} failed`)),
43
- );
44
- });
45
- }
20
+ [".env.example", ".env.example"],
21
+ [gitignoreSource, ".gitignore"],
22
+ [".prettierignore", ".prettierignore"],
23
+ ["README.md", "README.md"],
24
+ ["bun.lock", "bun.lock"],
25
+ ["eslint.config.mjs", "eslint.config.mjs"],
26
+ ["messages", "messages"],
27
+ ["next.config.ts", "next.config.ts"],
28
+ ["package.json", "package.json"],
29
+ ["playwright.config.ts", "playwright.config.ts"],
30
+ ["pnpm-workspace.yaml", "pnpm-workspace.yaml"],
31
+ ["postcss.config.mjs", "postcss.config.mjs"],
32
+ ["public", "public"],
33
+ ["scripts", "scripts"],
34
+ ["src", "src"],
35
+ ["tailwind.config.ts", "tailwind.config.ts"],
36
+ ["tests", "tests"],
37
+ ["tsconfig.json", "tsconfig.json"],
38
+ ["vitest.config.ts", "vitest.config.ts"],
39
+ ] as const;
46
40
 
47
41
  try {
48
- for (const entry of entries) {
49
- await cp(path.join(root, entry), path.join(target, entry), {
42
+ for (const [source, destination] of entries) {
43
+ await cp(path.join(root, source), path.join(target, destination), {
50
44
  recursive: true,
51
45
  });
52
46
  }
47
+
53
48
  if (manager !== "bun") {
54
49
  const packagePath = path.join(target, "package.json");
55
- const manifest = JSON.parse(await readFile(packagePath, "utf8"));
50
+ const manifest = JSON.parse(await readFile(packagePath, "utf8")) as Record<
51
+ string,
52
+ unknown
53
+ >;
56
54
  delete manifest.packageManager;
57
55
  await writeFile(packagePath, `${JSON.stringify(manifest, null, 2)}\n`);
58
56
  }
59
- await run(
60
- manager,
61
- manager === "bun" ? ["install", "--frozen-lockfile"] : ["install"],
62
- );
63
- await run(manager, ["run", "check"]);
57
+
58
+ const environment = {
59
+ ...process.env,
60
+ AUTH_DISABLED: "true",
61
+ CI: "true",
62
+ CNP_PACKAGE_MANAGER: manager,
63
+ };
64
+ await runPackageManager(manager, installArguments(manager), {
65
+ cwd: target,
66
+ env: environment,
67
+ });
68
+ await runPackageManager(manager, ["run", "check"], {
69
+ cwd: target,
70
+ env: environment,
71
+ });
72
+ await stat(path.join(target, "next-env.d.ts"));
64
73
  } finally {
65
74
  await rm(target, { recursive: true, force: true });
66
75
  }
@@ -1,8 +1,8 @@
1
1
  import { expect, test } from "@playwright/test";
2
2
 
3
3
  const captureByProject: Record<string, string> = {
4
- desktop: "artifacts/captures/phase-2-template-integration-desktop.png",
5
- mobile: "artifacts/captures/phase-2-template-integration-mobile.png",
4
+ desktop: "artifacts/captures/phase-2-11-template-reconciliation-desktop.png",
5
+ mobile: "artifacts/captures/phase-2-11-template-reconciliation-mobile.png",
6
6
  };
7
7
 
8
8
  test("public template routes render in both locales", async ({
@@ -16,14 +16,57 @@ test("public template routes render in both locales", async ({
16
16
  await expect(page.getByRole("banner")).toBeVisible();
17
17
  const homeHeading = page
18
18
  .getByRole("main")
19
- .getByRole("heading", { name: "Home" });
19
+ .getByRole("heading", { name: "Welcome to your website" });
20
20
  await expect(homeHeading).toBeVisible();
21
+ await expect(page.getByText("Start developing right now.")).toBeVisible();
22
+ await expect(
23
+ page.getByText("This project was created with create-next-pro-cli"),
24
+ ).toBeVisible();
25
+ await expect(page.getByRole("contentinfo")).toContainText(
26
+ "Empowered by Rising Corporation",
27
+ );
28
+ const readmeLink = page.getByRole("link", {
29
+ name: "Read the Doc",
30
+ });
31
+ await expect(readmeLink).toHaveAttribute(
32
+ "href",
33
+ "https://github.com/Rising-Corporation/create-next-pro-cli#readme",
34
+ );
35
+ const githubActions = page.getByRole("main");
36
+ for (const action of ["Star", "Watch"]) {
37
+ const actionLink = githubActions.getByRole("link", {
38
+ name: new RegExp(`^${action}`),
39
+ });
40
+ await expect(actionLink).toHaveAttribute(
41
+ "href",
42
+ "https://github.com/Rising-Corporation/create-next-pro-cli",
43
+ );
44
+ await expect(actionLink).toHaveAttribute("target", "_blank");
45
+ await expect(actionLink).toHaveAttribute("rel", "noreferrer");
46
+ }
21
47
  await page.getByLabel("Toggle theme").click();
22
48
  await expect(page.locator("html")).toHaveClass(/dark/);
23
49
  await expect(homeHeading).toHaveCSS("color", "rgb(255, 255, 255)");
24
50
  await page.getByLabel("Toggle theme").click();
25
51
  await expect(page.locator("html")).toHaveClass(/light/);
26
52
 
53
+ await page.goto("/fr");
54
+ await expect(
55
+ page.getByRole("heading", { name: "Bienvenue sur votre site" }),
56
+ ).toBeVisible();
57
+ await expect(
58
+ page.getByText("Commencez votre développement dès maintenant."),
59
+ ).toBeVisible();
60
+ await expect(
61
+ page.getByText("Ce projet est créé avec create-next-pro-cli"),
62
+ ).toBeVisible();
63
+ await expect(
64
+ page.getByRole("link", { name: "Lire la documentation" }),
65
+ ).toHaveAttribute(
66
+ "href",
67
+ "https://github.com/Rising-Corporation/create-next-pro-cli#readme",
68
+ );
69
+
27
70
  await page.goto("/fr/login");
28
71
  await expect(page.getByRole("heading", { name: /connexion/i })).toBeVisible();
29
72
  await expect(page.getByLabel("Choisir la langue")).toHaveValue("fr");
@@ -35,7 +78,7 @@ test("public template routes render in both locales", async ({
35
78
 
36
79
  const capturePath = captureByProject[testInfo.project.name];
37
80
  if (capturePath) {
38
- await page.goto("/fr/login");
81
+ await page.goto("/fr");
39
82
  await page.mouse.move(0, 0);
40
83
  await page.screenshot({ path: capturePath, fullPage: true });
41
84
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "vitest";
2
- import { createContentSecurityPolicy } from "../../src/lib/security/csp";
2
+ import { createContentSecurityPolicy } from "@/lib/security/csp";
3
3
 
4
4
  describe("Content Security Policy", () => {
5
5
  test("allows the stable inline scripts required by static Next.js pages", () => {
@@ -0,0 +1,45 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ getGitHubRepositoryStats,
4
+ parseGitHubRepositoryStats,
5
+ } from "@/lib/github/repository";
6
+
7
+ describe("GitHub repository statistics", () => {
8
+ test("maps stars and actual watchers from the repository response", () => {
9
+ expect(
10
+ parseGitHubRepositoryStats({
11
+ stargazers_count: 12,
12
+ watchers_count: 12,
13
+ subscribers_count: 4,
14
+ }),
15
+ ).toEqual({ stars: 12, watchers: 4 });
16
+ });
17
+
18
+ test.each([
19
+ null,
20
+ {},
21
+ { stargazers_count: -1, subscribers_count: 2 },
22
+ { stargazers_count: 1, subscribers_count: "2" },
23
+ ])("rejects invalid repository statistics", (value) => {
24
+ expect(parseGitHubRepositoryStats(value)).toEqual({
25
+ stars: null,
26
+ watchers: null,
27
+ });
28
+ });
29
+
30
+ test("falls back when GitHub returns an error response", async () => {
31
+ const stats = await getGitHubRepositoryStats(
32
+ async () => new Response(null, { status: 403 }),
33
+ );
34
+
35
+ expect(stats).toEqual({ stars: null, watchers: null });
36
+ });
37
+
38
+ test("falls back when the GitHub request fails", async () => {
39
+ const stats = await getGitHubRepositoryStats(async () => {
40
+ throw new Error("network unavailable");
41
+ });
42
+
43
+ expect(stats).toEqual({ stars: null, watchers: null });
44
+ });
45
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ installArguments,
4
+ packageManagerExecutable,
5
+ resolvePackageManager,
6
+ } from "../../scripts/package-manager.ts";
7
+
8
+ describe("package manager dispatch", () => {
9
+ test.each(["bun", "npm", "pnpm"] as const)(
10
+ "accepts the explicit %s override",
11
+ (manager) => {
12
+ expect(resolvePackageManager({ CNP_PACKAGE_MANAGER: manager })).toBe(
13
+ manager,
14
+ );
15
+ },
16
+ );
17
+
18
+ test.each([
19
+ ["bun/1.3.14 npm/? node/v24.13.0", "bun"],
20
+ ["npm/11.0.0 node/v24.13.0", "npm"],
21
+ ["pnpm/11.0.0 npm/? node/v24.13.0", "pnpm"],
22
+ ] as const)("detects %s", (userAgent, manager) => {
23
+ expect(resolvePackageManager({ npm_config_user_agent: userAgent })).toBe(
24
+ manager,
25
+ );
26
+ });
27
+
28
+ test("rejects unknown and missing managers", () => {
29
+ expect(() =>
30
+ resolvePackageManager({ CNP_PACKAGE_MANAGER: "yarn" }),
31
+ ).toThrow("Unsupported package manager");
32
+ expect(() => resolvePackageManager({})).toThrow(
33
+ "Unable to detect the package manager",
34
+ );
35
+ });
36
+
37
+ test("uses Windows command shims without a shell", () => {
38
+ expect(packageManagerExecutable("npm", "win32")).toBe("npm.cmd");
39
+ expect(packageManagerExecutable("pnpm", "win32")).toBe("pnpm.cmd");
40
+ expect(packageManagerExecutable("bun", "win32")).toBe("bun");
41
+ });
42
+
43
+ test("uses reproducible install arguments where supported", () => {
44
+ expect(installArguments("bun")).toEqual(["install", "--frozen-lockfile"]);
45
+ expect(installArguments("npm")).toEqual(["install"]);
46
+ expect(installArguments("pnpm")).toEqual([
47
+ "install",
48
+ "--no-frozen-lockfile",
49
+ ]);
50
+ });
51
+ });
@@ -0,0 +1,52 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import path from "node:path";
4
+ import { describe, expect, test } from "vitest";
5
+
6
+ const root = fileURLToPath(new URL("../../", import.meta.url));
7
+
8
+ async function source(relativePath: string) {
9
+ return readFile(path.join(root, relativePath), "utf8");
10
+ }
11
+
12
+ describe("updated template baseline", () => {
13
+ test.each([
14
+ ["src/lib/github/repository.ts", "getGitHubRepositoryStats"],
15
+ ["src/ui/_home/GitHubActions.tsx", "GITHUB_REPOSITORY_URL"],
16
+ ["src/ui/_home/page-shell.tsx", "Empowered by Rising Corporation"],
17
+ ["src/ui/_global/UserNav.tsx", "ThemeToggle"],
18
+ ["next.config.ts", "lh3.googleusercontent.com"],
19
+ ["tests/e2e/template-remediation.playwright.ts", "Read the Doc"],
20
+ ])("preserves %s", async (relativePath, marker) => {
21
+ await expect(source(relativePath)).resolves.toContain(marker);
22
+ });
23
+
24
+ test.each(["en", "fr"])(
25
+ "preserves the updated %s home messages",
26
+ async (locale) => {
27
+ const messages = JSON.parse(
28
+ await source(`messages/${locale}/_home.json`),
29
+ ) as Record<string, unknown>;
30
+ expect(messages).toHaveProperty("readme");
31
+ expect(messages).toHaveProperty("github");
32
+ },
33
+ );
34
+
35
+ test("keeps a complete canonical authentication example without exposing values", async () => {
36
+ const variables = new Map<string, string>();
37
+ for (const line of (await source(".env.example")).split(/\r?\n/)) {
38
+ if (!line || /^\s*#/.test(line)) continue;
39
+ const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)$/);
40
+ if (!match) continue;
41
+ variables.set(match[1], match[2].trim().replace(/\s+#.*$/, ""));
42
+ }
43
+
44
+ const configuredAuthValues = [
45
+ variables.get("AUTH_SECRET") ?? variables.get("NEXTAUTH_SECRET"),
46
+ variables.get("AUTH_GOOGLE_ID") ?? variables.get("GOOGLE_CLIENT_ID"),
47
+ variables.get("AUTH_GOOGLE_SECRET") ??
48
+ variables.get("GOOGLE_CLIENT_SECRET"),
49
+ ].filter(Boolean);
50
+ expect(configuredAuthValues).toHaveLength(3);
51
+ });
52
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { NextIntlClientProvider } from "next-intl";
3
+ import React from "react";
4
+ import { renderToStaticMarkup } from "react-dom/server";
5
+ import en from "../../messages/en";
6
+ import fr from "../../messages/fr";
7
+ import UserNav from "../../src/ui/_global/UserNav";
8
+
9
+ vi.mock("@/lib/i18n/navigation", async () => {
10
+ const { useLocale } = await import("next-intl");
11
+ return {
12
+ Link: ({ href, children, ...props }: React.ComponentProps<"a">) => {
13
+ const locale = useLocale();
14
+ return React.createElement(
15
+ "a",
16
+ { href: `/${locale}${href}`, ...props },
17
+ children,
18
+ );
19
+ },
20
+ };
21
+ });
22
+
23
+ const renderUserNav = (locale: "en" | "fr", messages: typeof en | typeof fr) =>
24
+ renderToStaticMarkup(
25
+ <NextIntlClientProvider
26
+ locale={locale}
27
+ messages={messages}
28
+ onError={() => undefined}
29
+ >
30
+ <UserNav />
31
+ </NextIntlClientProvider>,
32
+ );
33
+
34
+ describe("UserNav", () => {
35
+ test.each([
36
+ ["en", en, "Toggle theme", "Logout"],
37
+ ["fr", fr, "Changer de thème", "Déconnexion"],
38
+ ] as const)(
39
+ "renders one localized theme control and keeps the %s navigation actions",
40
+ (locale, messages, themeLabel, logoutLabel) => {
41
+ const html = renderUserNav(locale, messages);
42
+
43
+ expect(
44
+ html.match(new RegExp(`aria-label="${themeLabel}"`, "g")),
45
+ ).toHaveLength(1);
46
+ expect(html).toContain(`href="/${locale}/dashboard"`);
47
+ expect(html).toContain(`href="/${locale}/settings"`);
48
+ expect(html).toContain(`href="/${locale}/userInfo"`);
49
+ expect(html).toContain(logoutLabel);
50
+ },
51
+ );
52
+ });
@@ -9,11 +9,12 @@
9
9
  "esModuleInterop": true,
10
10
  "module": "esnext",
11
11
  "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
12
13
  "resolveJsonModule": true,
13
14
  "isolatedModules": true,
14
15
  "jsx": "react-jsx",
15
16
  "incremental": true,
16
- "types": ["node", "vitest/globals"],
17
+ "types": ["node", "vitest/globals", "react-dom", "react"],
17
18
  "plugins": [
18
19
  {
19
20
  "name": "next"
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ resolve: {
5
+ tsconfigPaths: true,
6
+ },
7
+ test: {
8
+ include: ["tests/unit/**/*.test.{ts,tsx}"],
9
+ },
10
+ });