newstack 0.1.0__py3-none-any.whl

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 (72) hide show
  1. newstack/__init__.py +4 -0
  2. newstack/__main__.py +8 -0
  3. newstack/cli.py +387 -0
  4. newstack/data/stacks/fastapi/stack.json +42 -0
  5. newstack/data/stacks/fastapi/template/.github/workflows/ci.yml +30 -0
  6. newstack/data/stacks/fastapi/template/.gitignore +11 -0
  7. newstack/data/stacks/fastapi/template/Dockerfile +13 -0
  8. newstack/data/stacks/fastapi/template/LICENSE +21 -0
  9. newstack/data/stacks/fastapi/template/README.md +30 -0
  10. newstack/data/stacks/fastapi/template/pyproject.toml +31 -0
  11. newstack/data/stacks/fastapi/template/src/{{package_name}}/__init__.py +4 -0
  12. newstack/data/stacks/fastapi/template/src/{{package_name}}/main.py +50 -0
  13. newstack/data/stacks/fastapi/template/tests/test_api.py +41 -0
  14. newstack/data/stacks/node-express-ts/stack.json +14 -0
  15. newstack/data/stacks/node-express-ts/template/.github/workflows/ci.yml +28 -0
  16. newstack/data/stacks/node-express-ts/template/.gitignore +6 -0
  17. newstack/data/stacks/node-express-ts/template/LICENSE +21 -0
  18. newstack/data/stacks/node-express-ts/template/README.md +24 -0
  19. newstack/data/stacks/node-express-ts/template/package.json +26 -0
  20. newstack/data/stacks/node-express-ts/template/src/app.ts +35 -0
  21. newstack/data/stacks/node-express-ts/template/src/server.ts +7 -0
  22. newstack/data/stacks/node-express-ts/template/tests/app.test.ts +32 -0
  23. newstack/data/stacks/node-express-ts/template/tsconfig.json +14 -0
  24. newstack/data/stacks/python-cli/stack.json +50 -0
  25. newstack/data/stacks/python-cli/template/.github/workflows/ci.yml +32 -0
  26. newstack/data/stacks/python-cli/template/.gitignore +11 -0
  27. newstack/data/stacks/python-cli/template/LICENSE +21 -0
  28. newstack/data/stacks/python-cli/template/README.md +35 -0
  29. newstack/data/stacks/python-cli/template/pyproject.toml +32 -0
  30. newstack/data/stacks/python-cli/template/src/{{package_name}}/__init__.py +4 -0
  31. newstack/data/stacks/python-cli/template/src/{{package_name}}/__main__.py +6 -0
  32. newstack/data/stacks/python-cli/template/src/{{package_name}}/cli.py +30 -0
  33. newstack/data/stacks/python-cli/template/tests/test_cli.py +20 -0
  34. newstack/data/stacks/python-lib/stack.json +42 -0
  35. newstack/data/stacks/python-lib/template/.github/workflows/ci.yml +32 -0
  36. newstack/data/stacks/python-lib/template/.gitignore +11 -0
  37. newstack/data/stacks/python-lib/template/LICENSE +21 -0
  38. newstack/data/stacks/python-lib/template/README.md +28 -0
  39. newstack/data/stacks/python-lib/template/pyproject.toml +34 -0
  40. newstack/data/stacks/python-lib/template/src/{{package_name}}/__init__.py +6 -0
  41. newstack/data/stacks/python-lib/template/src/{{package_name}}/core.py +23 -0
  42. newstack/data/stacks/python-lib/template/tests/test_core.py +31 -0
  43. newstack/data/stacks/react-vite-ts/stack.json +15 -0
  44. newstack/data/stacks/react-vite-ts/template/.github/workflows/ci.yml +29 -0
  45. newstack/data/stacks/react-vite-ts/template/.gitignore +7 -0
  46. newstack/data/stacks/react-vite-ts/template/LICENSE +21 -0
  47. newstack/data/stacks/react-vite-ts/template/README.md +25 -0
  48. newstack/data/stacks/react-vite-ts/template/index.html +12 -0
  49. newstack/data/stacks/react-vite-ts/template/package.json +28 -0
  50. newstack/data/stacks/react-vite-ts/template/src/App.test.tsx +20 -0
  51. newstack/data/stacks/react-vite-ts/template/src/App.tsx +15 -0
  52. newstack/data/stacks/react-vite-ts/template/src/main.tsx +16 -0
  53. newstack/data/stacks/react-vite-ts/template/src/styles.css +26 -0
  54. newstack/data/stacks/react-vite-ts/template/tsconfig.json +16 -0
  55. newstack/data/stacks/react-vite-ts/template/vite.config.ts +10 -0
  56. newstack/data/stacks/static-site/stack.json +14 -0
  57. newstack/data/stacks/static-site/template/.github/workflows/deploy-pages.yml +30 -0
  58. newstack/data/stacks/static-site/template/.gitignore +2 -0
  59. newstack/data/stacks/static-site/template/LICENSE +21 -0
  60. newstack/data/stacks/static-site/template/README.md +21 -0
  61. newstack/data/stacks/static-site/template/index.html +34 -0
  62. newstack/data/stacks/static-site/template/main.js +15 -0
  63. newstack/data/stacks/static-site/template/styles.css +74 -0
  64. newstack/data/stacks/static-site/template/tests/site.test.mjs +22 -0
  65. newstack/registry.py +137 -0
  66. newstack/render.py +163 -0
  67. newstack/verify.py +133 -0
  68. newstack-0.1.0.dist-info/METADATA +197 -0
  69. newstack-0.1.0.dist-info/RECORD +72 -0
  70. newstack-0.1.0.dist-info/WHEEL +4 -0
  71. newstack-0.1.0.dist-info/entry_points.txt +2 -0
  72. newstack-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,23 @@
1
+ """Core helpers for {{ title }}."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ _WORD_RE = re.compile(r"[A-Za-z0-9']+")
8
+
9
+
10
+ def word_count(text: str) -> int:
11
+ """Return the number of words in *text*."""
12
+ return len(_WORD_RE.findall(text))
13
+
14
+
15
+ def truncate(text: str, limit: int, suffix: str = "…") -> str:
16
+ """Shorten *text* to at most *limit* characters, appending *suffix* when cut."""
17
+ if limit < 0:
18
+ raise ValueError("limit must be >= 0")
19
+ if len(text) <= limit:
20
+ return text
21
+ if limit <= len(suffix):
22
+ return suffix[:limit]
23
+ return text[: limit - len(suffix)].rstrip() + suffix
@@ -0,0 +1,31 @@
1
+ import pytest
2
+
3
+ from {{ package_name }} import truncate, word_count
4
+
5
+
6
+ def test_version_is_exposed() -> None:
7
+ import {{ package_name }}
8
+
9
+ assert {{ package_name }}.__version__ == "0.1.0"
10
+
11
+
12
+ @pytest.mark.parametrize(
13
+ ("text", "expected"),
14
+ [("", 0), ("hello", 1), ("hello world", 2), ("it's a test, isn't it?", 5)],
15
+ )
16
+ def test_word_count(text: str, expected: int) -> None:
17
+ assert word_count(text) == expected
18
+
19
+
20
+ def test_truncate_leaves_short_text_alone() -> None:
21
+ assert truncate("hello", 10) == "hello"
22
+
23
+
24
+ def test_truncate_cuts_long_text() -> None:
25
+ assert truncate("hello world", 8) == "hello w…"
26
+ assert len(truncate("x" * 100, 20)) == 20
27
+
28
+
29
+ def test_truncate_rejects_negative_limit() -> None:
30
+ with pytest.raises(ValueError):
31
+ truncate("abc", -1)
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "react-vite-ts",
3
+ "title": "React + Vite + TypeScript",
4
+ "language": "node",
5
+ "blurb": "SPA with Vitest + Testing Library, typed build, CI",
6
+ "requires": ["node", "npm", "git"],
7
+ "default_description": "A React single page application.",
8
+ "next_steps": ["cd {path}", "npm install", "npm run dev"],
9
+ "verify": [
10
+ {"name": "install", "cmd": "npm install --no-audit --no-fund --silent", "tools": ["npm"], "timeout": 900},
11
+ {"name": "typecheck", "cmd": "npm run typecheck", "tools": ["npm"], "timeout": 300},
12
+ {"name": "test", "cmd": "npm test", "tools": ["npm"], "timeout": 300},
13
+ {"name": "build", "cmd": "npm run build", "tools": ["npm"], "timeout": 300}
14
+ ]
15
+ }
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ node-version: ["22", "24"]
18
+ steps:
19
+ - uses: actions/checkout@v7
20
+ - uses: actions/setup-node@v7
21
+ with:
22
+ node-version: ${{ matrix.node-version }}
23
+ cache: npm
24
+ # no lockfile is shipped yet: run `npm install` once locally and commit
25
+ # package-lock.json, then switch this to `npm ci` for reproducible installs
26
+ - run: npm install --no-audit --no-fund
27
+ - run: npm run typecheck
28
+ - run: npm test
29
+ - run: npm run build
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ dist/
3
+ .vite/
4
+ coverage/
5
+ *.local
6
+ .DS_Store
7
+ npm-debug.log*
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) {{ year }} {{ author }}
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,25 @@
1
+ # {{ title }}
2
+
3
+ {{ description }}
4
+
5
+ React 19 + TypeScript on Vite, tested with Vitest and Testing Library.
6
+
7
+ ## Develop
8
+
9
+ ```bash
10
+ npm install
11
+ npm run dev # http://localhost:5173
12
+ ```
13
+
14
+ ## Check
15
+
16
+ ```bash
17
+ npm run typecheck # tsc --noEmit
18
+ npm test # vitest run
19
+ npm run build # production bundle in dist/
20
+ ```
21
+
22
+ After the first `npm install`, commit `package-lock.json` and switch CI's install step
23
+ to `npm ci` for reproducible installs.
24
+
25
+ Generated with [newstack](https://github.com/DawnofGenX/newstack).
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{ title }}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "{{ npm_name }}",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "description": "{{ description }}",
7
+ "scripts": {
8
+ "dev": "vite",
9
+ "build": "tsc --noEmit && vite build",
10
+ "preview": "vite preview",
11
+ "test": "vitest run",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "dependencies": {
15
+ "react": "^19.3.1",
16
+ "react-dom": "^19.3.1"
17
+ },
18
+ "devDependencies": {
19
+ "@testing-library/react": "^16.3.3",
20
+ "@types/react": "^19.3.0",
21
+ "@types/react-dom": "^19.3.0",
22
+ "@vitejs/plugin-react": "^6.1.2",
23
+ "jsdom": "^30.1.3",
24
+ "typescript": "^7.0.2",
25
+ "vite": "^8.3.5",
26
+ "vitest": "^5.0.5"
27
+ }
28
+ }
@@ -0,0 +1,20 @@
1
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+
4
+ import App from "./App";
5
+
6
+ afterEach(cleanup);
7
+
8
+ describe("App", () => {
9
+ it("renders the project heading", () => {
10
+ render(<App />);
11
+ expect(screen.getByRole("heading", { level: 1 }).textContent).toContain("{{ title }}");
12
+ });
13
+
14
+ it("increments the counter", () => {
15
+ render(<App />);
16
+ expect(screen.getByText("Count: 0")).toBeTruthy();
17
+ fireEvent.click(screen.getByRole("button", { name: /increment/i }));
18
+ expect(screen.getByText("Count: 1")).toBeTruthy();
19
+ });
20
+ });
@@ -0,0 +1,15 @@
1
+ import { useState } from "react";
2
+
3
+ export default function App() {
4
+ const [count, setCount] = useState(0);
5
+
6
+ return (
7
+ <main className="app">
8
+ <h1>{{ title }}</h1>
9
+ <p>Count: {count}</p>
10
+ <button type="button" onClick={() => setCount((value) => value + 1)}>
11
+ Increment
12
+ </button>
13
+ </main>
14
+ );
15
+ }
@@ -0,0 +1,16 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+
4
+ import App from "./App";
5
+ import "./styles.css";
6
+
7
+ const container = document.getElementById("root");
8
+ if (!container) {
9
+ throw new Error("#root element is missing from index.html");
10
+ }
11
+
12
+ createRoot(container).render(
13
+ <StrictMode>
14
+ <App />
15
+ </StrictMode>,
16
+ );
@@ -0,0 +1,26 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
4
+ line-height: 1.5;
5
+ }
6
+
7
+ body {
8
+ margin: 0;
9
+ min-height: 100vh;
10
+ display: grid;
11
+ place-items: center;
12
+ }
13
+
14
+ .app {
15
+ text-align: center;
16
+ padding: 2rem;
17
+ }
18
+
19
+ button {
20
+ font: inherit;
21
+ padding: 0.5rem 1.25rem;
22
+ border-radius: 0.5rem;
23
+ border: 1px solid currentColor;
24
+ background: transparent;
25
+ cursor: pointer;
26
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "isolatedModules": true,
12
+ "esModuleInterop": true,
13
+ "types": ["vite/client"]
14
+ },
15
+ "include": ["src", "vite.config.ts"]
16
+ }
@@ -0,0 +1,10 @@
1
+ import react from "@vitejs/plugin-react";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ test: {
7
+ environment: "jsdom",
8
+ include: ["src/**/*.test.{ts,tsx}"],
9
+ },
10
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "static-site",
3
+ "title": "Static site",
4
+ "language": "html",
5
+ "blurb": "HTML + CSS + JS landing page: node:test suite, GitHub Pages deploy",
6
+ "requires": ["node", "git"],
7
+ "default_description": "A static website.",
8
+ "next_steps": ["cd {path}", "node --test \"tests/**/*.test.mjs\"", "python3 -m http.server 8000"],
9
+ "verify": [
10
+ {"name": "test", "cmd": "node --test \"tests/**/*.test.mjs\"", "tools": ["node"], "timeout": 120},
11
+ {"name": "js-syntax", "cmd": "node --check main.js", "tools": ["node"], "timeout": 120},
12
+ {"name": "assets", "cmd": "test -f index.html && test -f styles.css && test -f main.js", "tools": [], "timeout": 60}
13
+ ]
14
+ }
@@ -0,0 +1,30 @@
1
+ name: Deploy to GitHub Pages
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ pages: write
11
+ id-token: write
12
+
13
+ concurrency:
14
+ group: pages
15
+ cancel-in-progress: true
16
+
17
+ jobs:
18
+ deploy:
19
+ runs-on: ubuntu-latest
20
+ environment:
21
+ name: github-pages
22
+ url: ${{ steps.deployment.outputs.page_url }}
23
+ steps:
24
+ - uses: actions/checkout@v7
25
+ - uses: actions/configure-pages@v6
26
+ - uses: actions/upload-pages-artifact@v5
27
+ with:
28
+ path: .
29
+ - id: deployment
30
+ uses: actions/deploy-pages@v5
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ node_modules/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) {{ year }} {{ author }}
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ # {{ title }}
2
+
3
+ {{ description }}
4
+
5
+ Zero-build static site: plain HTML, CSS and JavaScript. Push to `main` and the
6
+ included workflow publishes it to GitHub Pages.
7
+
8
+ ## Preview locally
9
+
10
+ ```bash
11
+ python3 -m http.server 8000
12
+ # or: npx serve .
13
+ ```
14
+
15
+ ## Publish
16
+
17
+ 1. Push this repository to GitHub.
18
+ 2. Settings → Pages → Source: **GitHub Actions**.
19
+ 3. The next push to `main` deploys it.
20
+
21
+ Generated with [newstack](https://github.com/DawnofGenX/newstack).
@@ -0,0 +1,34 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>{{ title }}</title>
7
+ <meta name="description" content="{{ description }}" />
8
+ <link rel="stylesheet" href="styles.css" />
9
+ </head>
10
+ <body>
11
+ <header class="hero">
12
+ <h1>{{ title }}</h1>
13
+ <p>{{ description }}</p>
14
+ <a class="cta" href="#learn-more">Get started</a>
15
+ </header>
16
+
17
+ <main id="learn-more">
18
+ <section>
19
+ <h2>Fast by default</h2>
20
+ <p>No build step, no dependencies, no framework. Just files.</p>
21
+ </section>
22
+ <section>
23
+ <h2>Deploy on push</h2>
24
+ <p>The included workflow publishes this page to GitHub Pages.</p>
25
+ </section>
26
+ </main>
27
+
28
+ <footer>
29
+ <p>&copy; <span id="year"></span> {{ author }}</p>
30
+ </footer>
31
+
32
+ <script src="main.js"></script>
33
+ </body>
34
+ </html>
@@ -0,0 +1,15 @@
1
+ const year = document.getElementById("year");
2
+ if (year) {
3
+ year.textContent = String(new Date().getFullYear());
4
+ }
5
+
6
+ for (const link of document.querySelectorAll('a[href^="#"]')) {
7
+ link.addEventListener("click", (event) => {
8
+ const targetId = link.getAttribute("href");
9
+ if (!targetId || targetId === "#") return;
10
+ const target = document.querySelector(targetId);
11
+ if (!target) return;
12
+ event.preventDefault();
13
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
14
+ });
15
+ }
@@ -0,0 +1,74 @@
1
+ :root {
2
+ --bg: #0b0d12;
3
+ --fg: #e8eaf0;
4
+ --muted: #9aa3b2;
5
+ --accent: #6ee7b7;
6
+ color-scheme: dark;
7
+ }
8
+
9
+ * {
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ body {
14
+ margin: 0;
15
+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
16
+ background: var(--bg);
17
+ color: var(--fg);
18
+ line-height: 1.6;
19
+ }
20
+
21
+ .hero {
22
+ min-height: 70vh;
23
+ display: grid;
24
+ align-content: center;
25
+ justify-items: start;
26
+ gap: 1rem;
27
+ padding: 4rem 8vw;
28
+ }
29
+
30
+ .hero h1 {
31
+ font-size: clamp(2.5rem, 6vw, 4.5rem);
32
+ margin: 0;
33
+ letter-spacing: -0.02em;
34
+ }
35
+
36
+ .hero p {
37
+ color: var(--muted);
38
+ font-size: 1.25rem;
39
+ max-width: 40ch;
40
+ margin: 0;
41
+ }
42
+
43
+ .cta {
44
+ margin-top: 1rem;
45
+ display: inline-block;
46
+ padding: 0.7rem 1.4rem;
47
+ border-radius: 999px;
48
+ background: var(--accent);
49
+ color: #06231a;
50
+ font-weight: 600;
51
+ text-decoration: none;
52
+ }
53
+
54
+ main {
55
+ display: grid;
56
+ gap: 2rem;
57
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
58
+ padding: 2rem 8vw 4rem;
59
+ }
60
+
61
+ main h2 {
62
+ margin-bottom: 0.25rem;
63
+ }
64
+
65
+ main p {
66
+ color: var(--muted);
67
+ margin-top: 0;
68
+ }
69
+
70
+ footer {
71
+ padding: 2rem 8vw;
72
+ color: var(--muted);
73
+ border-top: 1px solid #1c212c;
74
+ }
@@ -0,0 +1,22 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import { test } from "node:test";
4
+
5
+ const read = (name) => readFile(new URL(`../${name}`, import.meta.url), "utf8");
6
+
7
+ test("index.html is a complete document", async () => {
8
+ const html = await read("index.html");
9
+ assert.match(html, /<!doctype html>/i);
10
+ assert.match(html, /<title>.+<\/title>/);
11
+ assert.match(html, /<main[^>]*>/);
12
+ assert.match(html, /href="styles\.css"/);
13
+ assert.match(html, /src="main\.js"/);
14
+ });
15
+
16
+ test("styles.css styles the hero block", async () => {
17
+ assert.match(await read("styles.css"), /\.hero\s*\{/);
18
+ });
19
+
20
+ test("main.js wires the footer year", async () => {
21
+ assert.match(await read("main.js"), /getElementById\("year"\)/);
22
+ });