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.
- newstack/__init__.py +4 -0
- newstack/__main__.py +8 -0
- newstack/cli.py +387 -0
- newstack/data/stacks/fastapi/stack.json +42 -0
- newstack/data/stacks/fastapi/template/.github/workflows/ci.yml +30 -0
- newstack/data/stacks/fastapi/template/.gitignore +11 -0
- newstack/data/stacks/fastapi/template/Dockerfile +13 -0
- newstack/data/stacks/fastapi/template/LICENSE +21 -0
- newstack/data/stacks/fastapi/template/README.md +30 -0
- newstack/data/stacks/fastapi/template/pyproject.toml +31 -0
- newstack/data/stacks/fastapi/template/src/{{package_name}}/__init__.py +4 -0
- newstack/data/stacks/fastapi/template/src/{{package_name}}/main.py +50 -0
- newstack/data/stacks/fastapi/template/tests/test_api.py +41 -0
- newstack/data/stacks/node-express-ts/stack.json +14 -0
- newstack/data/stacks/node-express-ts/template/.github/workflows/ci.yml +28 -0
- newstack/data/stacks/node-express-ts/template/.gitignore +6 -0
- newstack/data/stacks/node-express-ts/template/LICENSE +21 -0
- newstack/data/stacks/node-express-ts/template/README.md +24 -0
- newstack/data/stacks/node-express-ts/template/package.json +26 -0
- newstack/data/stacks/node-express-ts/template/src/app.ts +35 -0
- newstack/data/stacks/node-express-ts/template/src/server.ts +7 -0
- newstack/data/stacks/node-express-ts/template/tests/app.test.ts +32 -0
- newstack/data/stacks/node-express-ts/template/tsconfig.json +14 -0
- newstack/data/stacks/python-cli/stack.json +50 -0
- newstack/data/stacks/python-cli/template/.github/workflows/ci.yml +32 -0
- newstack/data/stacks/python-cli/template/.gitignore +11 -0
- newstack/data/stacks/python-cli/template/LICENSE +21 -0
- newstack/data/stacks/python-cli/template/README.md +35 -0
- newstack/data/stacks/python-cli/template/pyproject.toml +32 -0
- newstack/data/stacks/python-cli/template/src/{{package_name}}/__init__.py +4 -0
- newstack/data/stacks/python-cli/template/src/{{package_name}}/__main__.py +6 -0
- newstack/data/stacks/python-cli/template/src/{{package_name}}/cli.py +30 -0
- newstack/data/stacks/python-cli/template/tests/test_cli.py +20 -0
- newstack/data/stacks/python-lib/stack.json +42 -0
- newstack/data/stacks/python-lib/template/.github/workflows/ci.yml +32 -0
- newstack/data/stacks/python-lib/template/.gitignore +11 -0
- newstack/data/stacks/python-lib/template/LICENSE +21 -0
- newstack/data/stacks/python-lib/template/README.md +28 -0
- newstack/data/stacks/python-lib/template/pyproject.toml +34 -0
- newstack/data/stacks/python-lib/template/src/{{package_name}}/__init__.py +6 -0
- newstack/data/stacks/python-lib/template/src/{{package_name}}/core.py +23 -0
- newstack/data/stacks/python-lib/template/tests/test_core.py +31 -0
- newstack/data/stacks/react-vite-ts/stack.json +15 -0
- newstack/data/stacks/react-vite-ts/template/.github/workflows/ci.yml +29 -0
- newstack/data/stacks/react-vite-ts/template/.gitignore +7 -0
- newstack/data/stacks/react-vite-ts/template/LICENSE +21 -0
- newstack/data/stacks/react-vite-ts/template/README.md +25 -0
- newstack/data/stacks/react-vite-ts/template/index.html +12 -0
- newstack/data/stacks/react-vite-ts/template/package.json +28 -0
- newstack/data/stacks/react-vite-ts/template/src/App.test.tsx +20 -0
- newstack/data/stacks/react-vite-ts/template/src/App.tsx +15 -0
- newstack/data/stacks/react-vite-ts/template/src/main.tsx +16 -0
- newstack/data/stacks/react-vite-ts/template/src/styles.css +26 -0
- newstack/data/stacks/react-vite-ts/template/tsconfig.json +16 -0
- newstack/data/stacks/react-vite-ts/template/vite.config.ts +10 -0
- newstack/data/stacks/static-site/stack.json +14 -0
- newstack/data/stacks/static-site/template/.github/workflows/deploy-pages.yml +30 -0
- newstack/data/stacks/static-site/template/.gitignore +2 -0
- newstack/data/stacks/static-site/template/LICENSE +21 -0
- newstack/data/stacks/static-site/template/README.md +21 -0
- newstack/data/stacks/static-site/template/index.html +34 -0
- newstack/data/stacks/static-site/template/main.js +15 -0
- newstack/data/stacks/static-site/template/styles.css +74 -0
- newstack/data/stacks/static-site/template/tests/site.test.mjs +22 -0
- newstack/registry.py +137 -0
- newstack/render.py +163 -0
- newstack/verify.py +133 -0
- newstack-0.1.0.dist-info/METADATA +197 -0
- newstack-0.1.0.dist-info/RECORD +72 -0
- newstack-0.1.0.dist-info/WHEEL +4 -0
- newstack-0.1.0.dist-info/entry_points.txt +2 -0
- 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,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,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,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>© <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
|
+
});
|