@alxia/create 0.1.3 → 0.1.5
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.
- package/README.md +53 -18
- package/dist/copy.d.ts +11 -3
- package/dist/copy.d.ts.map +1 -1
- package/dist/index.js +23 -5
- package/dist/index.js.map +4 -4
- package/dist/registry.d.ts +11 -1
- package/dist/registry.d.ts.map +1 -1
- package/docs/README.md +2 -2
- package/docs/guide.md +176 -39
- package/docs/roadmap.md +23 -0
- package/docs/troubleshooting.md +97 -0
- package/package.json +1 -1
- package/templates/api/.vscode/extensions.json +3 -0
- package/templates/api/.vscode/settings.json +8 -0
- package/templates/api/Dockerfile +4 -2
- package/templates/api/README.md +32 -2
- package/templates/api/_biome.json +32 -0
- package/templates/api/package.json +25 -19
- package/templates/api/src/app.spec.ts +38 -38
- package/templates/api/src/app.ts +17 -17
- package/templates/api/src/server.ts +16 -2
- package/templates/api/tsconfig.json +23 -23
- package/templates/react-router/.vscode/extensions.json +3 -0
- package/templates/react-router/.vscode/settings.json +8 -0
- package/templates/react-router/Dockerfile +4 -2
- package/templates/react-router/README.md +27 -0
- package/templates/react-router/_biome.json +49 -0
- package/templates/react-router/app/app.css +3 -2
- package/templates/react-router/app/routes/home.tsx +1 -1
- package/templates/react-router/app/routes.ts +1 -1
- package/templates/react-router/package.json +7 -1
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
"name": "my-api",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "bun --watch src/server.ts",
|
|
7
|
+
"build": "bun build src/server.ts --target=bun --outdir=dist --minify --sourcemap=linked",
|
|
8
|
+
"start": "bun dist/server.js",
|
|
9
|
+
"test": "bun test",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"lint": "biome lint",
|
|
12
|
+
"format": "biome format --write",
|
|
13
|
+
"check": "biome check --write",
|
|
14
|
+
"check:ci": "biome ci",
|
|
15
|
+
"verify": "bun run check:ci && bun run typecheck && bun run test"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@alxia/core": "workspace:^",
|
|
19
|
+
"zod": "^4.2.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@alxia/client": "workspace:^",
|
|
23
|
+
"@biomejs/biome": "2.5.15",
|
|
24
|
+
"@types/bun": "^1.4.2",
|
|
25
|
+
"typescript": "^6.0.3"
|
|
26
|
+
}
|
|
21
27
|
}
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import { expect, test } from
|
|
2
|
-
import { client } from
|
|
3
|
-
import { apiKey, app } from
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { client } from "@alxia/client";
|
|
3
|
+
import { apiKey, app } from "./app";
|
|
4
4
|
|
|
5
|
-
const json = {
|
|
5
|
+
const json = { "content-type": "application/json", "x-api-key": apiKey };
|
|
6
6
|
|
|
7
|
-
test(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
test("creates a todo from JSON", async () => {
|
|
8
|
+
const response = await app.request("/todos", {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: json,
|
|
11
|
+
body: JSON.stringify({ title: "Write a route" }),
|
|
12
|
+
});
|
|
13
|
+
expect(response.status).toBe(201);
|
|
14
|
+
expect(await response.json()).toEqual({
|
|
15
|
+
id: expect.any(Number),
|
|
16
|
+
title: "Write a route",
|
|
17
|
+
done: false,
|
|
18
|
+
});
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
test(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
test("refuses an empty title with a 400 naming it", async () => {
|
|
22
|
+
const response = await app.request("/todos", {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: json,
|
|
25
|
+
body: JSON.stringify({ title: "" }),
|
|
26
|
+
});
|
|
27
|
+
expect(response.status).toBe(400);
|
|
28
|
+
expect((await response.json()).issues[0].path).toEqual(["title"]);
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
test(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
test("the typed client reads each status the route answers", async () => {
|
|
32
|
+
// Given the app itself, the client calls its fetch in process.
|
|
33
|
+
const api = client(app, { headers: { "x-api-key": apiKey } });
|
|
34
|
+
const created = await api.post("/todos", {
|
|
35
|
+
body: { title: "Call it typed" },
|
|
36
|
+
});
|
|
37
|
+
if (created.status !== 201) throw new Error(`got ${created.status}`);
|
|
38
|
+
expect(created.data.title).toBe("Call it typed"); // data is the Todo schema's type
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
const anonymous = await client(app).post("/todos", {
|
|
41
|
+
body: { title: "No key" },
|
|
42
|
+
});
|
|
43
|
+
expect(anonymous.status).toBe(401);
|
|
44
|
+
if (anonymous.status === 401)
|
|
45
|
+
expect(anonymous.data.error).toBe("unauthorized");
|
|
46
46
|
});
|
package/templates/api/src/app.ts
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { alxia, defineHook } from
|
|
2
|
-
import { z } from
|
|
1
|
+
import { alxia, defineHook } from "@alxia/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
const Todo = z.object({ id: z.number(), title: z.string(), done: z.boolean() });
|
|
5
5
|
const NewTodo = z.object({ title: z.string().min(1) });
|
|
6
6
|
|
|
7
7
|
/** Set API_KEY in the environment: this default is for development. */
|
|
8
|
-
export const apiKey = Bun.env[
|
|
8
|
+
export const apiKey = Bun.env["API_KEY"] ?? "dev-key";
|
|
9
9
|
|
|
10
10
|
// A hook of the routes it is given to: it answers 401 without the key, and
|
|
11
11
|
// that 401 joins the type of each, so the client reads it.
|
|
12
12
|
const requireKey = defineHook(({ request, reply }) =>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
request.headers.get("x-api-key") === apiKey
|
|
14
|
+
? undefined
|
|
15
|
+
: reply(401, { error: "unauthorized" as const }),
|
|
16
16
|
);
|
|
17
17
|
|
|
18
18
|
const todos: z.infer<typeof Todo>[] = [];
|
|
19
19
|
|
|
20
20
|
export const app = alxia()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
.decorate({ todos })
|
|
22
|
+
.post(
|
|
23
|
+
"/todos",
|
|
24
|
+
[requireKey],
|
|
25
|
+
{ body: NewTodo, response: { 201: Todo } },
|
|
26
|
+
({ body, todos, reply }) => {
|
|
27
|
+
const todo = { id: todos.length + 1, title: body.title, done: false };
|
|
28
|
+
todos.push(todo);
|
|
29
|
+
return reply.created(todo);
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
32
|
|
|
33
33
|
export type App = typeof app;
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import { app } from
|
|
1
|
+
import { app } from "./app";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Stop as the platform asks. In a container Bun is process 1, which a
|
|
4
|
+
// signal with no handler does not stop: `docker stop` would wait.
|
|
5
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
6
|
+
process.once(signal, () => {
|
|
7
|
+
void app.stop().then(
|
|
8
|
+
() => process.exit(0),
|
|
9
|
+
(error: unknown) => {
|
|
10
|
+
console.error(error);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const server = app.listen(Number(Bun.env["PORT"] ?? 3000));
|
|
4
18
|
console.log(`listening on ${server.url}`);
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"types": ["bun"],
|
|
4
|
+
"lib": ["ESNext", "DOM"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noImplicitOverride": true,
|
|
15
|
+
"noImplicitReturns": true,
|
|
16
|
+
"noFallthroughCasesInSwitch": true,
|
|
17
|
+
"noUncheckedIndexedAccess": true,
|
|
18
|
+
"exactOptionalPropertyTypes": true,
|
|
19
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true,
|
|
22
|
+
"forceConsistentCasingInFileNames": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["src"]
|
|
25
25
|
}
|
|
@@ -16,8 +16,10 @@ RUN bun run build
|
|
|
16
16
|
# command, and --no-install: with no node_modules, Bun would otherwise
|
|
17
17
|
# fetch a package the build left out from the registry at startup,
|
|
18
18
|
# where this fails.
|
|
19
|
-
# The server listens on PORT (3000) and HOST (0.0.0.0).
|
|
20
|
-
|
|
19
|
+
# The server listens on PORT (3000) and HOST (0.0.0.0). On Alpine, about
|
|
20
|
+
# 200 MB lighter: build/ is JavaScript, which runs the same on musl. A
|
|
21
|
+
# native addon built for glibc alone needs oven/bun:1 here.
|
|
22
|
+
FROM oven/bun:1-alpine
|
|
21
23
|
WORKDIR /app
|
|
22
24
|
ENV NODE_ENV=production
|
|
23
25
|
COPY --from=build /app/build ./build
|
|
@@ -78,6 +78,33 @@ Make sure to deploy the output of `bun run build`
|
|
|
78
78
|
│ └── server/ # Server-side code
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
## Lint and format
|
|
82
|
+
|
|
83
|
+
[Biome](https://biomejs.dev) lints and formats the project, as `biome.json`
|
|
84
|
+
sets it: Biome's recommended rules, spaces, double quotes, imports
|
|
85
|
+
sorted. What the build and `react-router typegen` write, `build/` and
|
|
86
|
+
`.react-router/`, is skipped.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
bun run check # lint, format and sort imports, fixing what it can
|
|
90
|
+
bun run lint # lint only
|
|
91
|
+
bun run format # format only, in place
|
|
92
|
+
bun run check:ci # what CI runs: changes nothing, fails on an error
|
|
93
|
+
bun run verify # check:ci, then typecheck, then build
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`bun run check:ci`, not `bun ci`: `bun ci` is Bun's frozen-lockfile
|
|
97
|
+
install. In VS Code, `.vscode/` recommends Biome's extension and formats
|
|
98
|
+
on save with it.
|
|
99
|
+
|
|
100
|
+
`@biomejs/biome` is pinned exactly, since a release of Biome may format
|
|
101
|
+
differently. To move it:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
bun add --dev --exact @biomejs/biome@latest
|
|
105
|
+
bunx biome migrate --write
|
|
106
|
+
```
|
|
107
|
+
|
|
81
108
|
## Styling
|
|
82
109
|
|
|
83
110
|
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"includes": ["**", "!!**/build", "!!**/.react-router"]
|
|
10
|
+
},
|
|
11
|
+
"formatter": {
|
|
12
|
+
"enabled": true,
|
|
13
|
+
"indentStyle": "space"
|
|
14
|
+
},
|
|
15
|
+
"css": {
|
|
16
|
+
"parser": {
|
|
17
|
+
"tailwindDirectives": true
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"linter": {
|
|
21
|
+
"enabled": true,
|
|
22
|
+
"rules": {
|
|
23
|
+
"preset": "recommended",
|
|
24
|
+
"correctness": {
|
|
25
|
+
"noEmptyPattern": "off"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"assist": {
|
|
30
|
+
"enabled": true,
|
|
31
|
+
"actions": {
|
|
32
|
+
"source": {
|
|
33
|
+
"organizeImports": "on"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"overrides": [
|
|
38
|
+
{
|
|
39
|
+
"includes": ["**/app/welcome/**"],
|
|
40
|
+
"linter": {
|
|
41
|
+
"rules": {
|
|
42
|
+
"a11y": {
|
|
43
|
+
"noSvgWithoutTitle": "off"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
|
|
3
3
|
@theme {
|
|
4
|
-
--font-sans:
|
|
5
|
-
"
|
|
4
|
+
--font-sans:
|
|
5
|
+
"Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
|
6
|
+
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
html,
|
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
"build": "react-router build",
|
|
7
7
|
"dev": "react-router dev",
|
|
8
8
|
"start": "bun build/server/index.js",
|
|
9
|
-
"typecheck": "react-router typegen && tsc"
|
|
9
|
+
"typecheck": "react-router typegen && tsc",
|
|
10
|
+
"lint": "biome lint",
|
|
11
|
+
"format": "biome format --write",
|
|
12
|
+
"check": "biome check --write",
|
|
13
|
+
"check:ci": "biome ci",
|
|
14
|
+
"verify": "bun run check:ci && bun run typecheck && bun run build"
|
|
10
15
|
},
|
|
11
16
|
"dependencies": {
|
|
12
17
|
"@alxia/core": "workspace:^",
|
|
@@ -19,6 +24,7 @@
|
|
|
19
24
|
"react-router": "^8.4.0"
|
|
20
25
|
},
|
|
21
26
|
"devDependencies": {
|
|
27
|
+
"@biomejs/biome": "2.5.15",
|
|
22
28
|
"@react-router/dev": "^8.4.0",
|
|
23
29
|
"@tailwindcss/vite": "^4.2.2",
|
|
24
30
|
"@types/node": "^22",
|