@alxia/create 0.1.0 → 0.1.2
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 +37 -6
- package/dist/copy.d.ts +24 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/index.js +61 -281
- package/dist/index.js.map +8 -9
- package/dist/main.d.ts.map +1 -1
- package/dist/registry.d.ts +28 -7
- package/dist/registry.d.ts.map +1 -1
- package/dist/versions.d.ts +0 -2
- package/dist/versions.d.ts.map +1 -1
- package/dist/write.d.ts +2 -1
- package/dist/write.d.ts.map +1 -1
- package/docs/README.md +3 -3
- package/docs/guide.md +104 -27
- package/docs/roadmap.md +35 -9
- package/docs/troubleshooting.md +58 -63
- package/package.json +5 -3
- package/templates/api/.dockerignore +6 -0
- package/templates/api/.env.example +3 -0
- package/templates/api/Dockerfile +22 -0
- package/templates/api/README.md +64 -0
- package/templates/api/gitignore +6 -0
- package/templates/api/package.json +21 -0
- package/templates/api/src/app.spec.ts +46 -0
- package/templates/api/src/app.ts +33 -0
- package/templates/api/src/server.ts +4 -0
- package/templates/api/tsconfig.json +25 -0
- package/templates/react-router/.dockerignore +4 -0
- package/templates/react-router/Dockerfile +29 -0
- package/templates/react-router/README.md +87 -0
- package/templates/react-router/_bunfig.toml +4 -0
- package/templates/react-router/app/app.css +15 -0
- package/templates/react-router/app/root.tsx +75 -0
- package/templates/react-router/app/routes/home.tsx +13 -0
- package/templates/react-router/app/routes.ts +3 -0
- package/templates/react-router/app/welcome/logo-dark.svg +23 -0
- package/templates/react-router/app/welcome/logo-light.svg +23 -0
- package/templates/react-router/app/welcome/welcome.tsx +89 -0
- package/templates/react-router/gitignore +7 -0
- package/templates/react-router/package.json +31 -0
- package/templates/react-router/public/favicon.ico +0 -0
- package/templates/react-router/react-router.config.ts +7 -0
- package/templates/react-router/tsconfig.json +26 -0
- package/templates/react-router/vite.config.ts +11 -0
- package/dist/templates/api.d.ts +0 -16
- package/dist/templates/api.d.ts.map +0 -1
- package/dist/templates/react-router.d.ts +0 -26
- package/dist/templates/react-router.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -28,8 +28,8 @@ bun create @alxia my-site --template react-router
|
|
|
28
28
|
|
|
29
29
|
| template | what it writes |
|
|
30
30
|
| --- | --- |
|
|
31
|
-
| `api` | an `@alxia/core` app with Zod: `POST /todos` validates its body, behind `requireKey`, a hook made with `defineHook` that answers 401 without an API key; a `bun test` spec calling it with `app.request()` and through `@alxia/client`, typed; `bun dev` restarting on change, `typecheck`, `build`, a strict `tsconfig.json`, `.gitignore` and a README |
|
|
32
|
-
| `react-router` | React Router's
|
|
31
|
+
| `api` | an `@alxia/core` app with Zod: `POST /todos` validates its body, behind `requireKey`, a hook made with `defineHook` that answers 401 without an API key; a `bun test` spec calling it with `app.request()` and through `@alxia/client`, typed; `bun dev` restarting on change, `typecheck`, `build`, a strict `tsconfig.json`, a `Dockerfile` on `oven/bun:1`, `.dockerignore`, `.gitignore`, `.env.example` and a README |
|
|
32
|
+
| `react-router` | React Router's official template, as `create-react-router` writes it, shipped in this package and copied, with [`@alxia/react-router`](https://www.npmjs.com/package/@alxia/react-router) added as its README says: `alxia()` in `vite.config.ts`'s plugins, `start` running `bun build/server/index.js`, a `bunfig.toml` starting React Router's CLI on Bun, and a `Dockerfile` on `oven/bun:1` in place of React Router's Node one. No server file: the default one serves the pages; `bunx alxia-react-router reveal` writes it out to customise |
|
|
33
33
|
|
|
34
34
|
The heart of the `api` project, its route and hook (the whole file, with
|
|
35
35
|
its imports and schemas, is in the [guide](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md#the-api-template)):
|
|
@@ -51,6 +51,33 @@ export const app = alxia()
|
|
|
51
51
|
});
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
## Docker
|
|
55
|
+
|
|
56
|
+
Both projects build into an image as they are written, on `oven/bun:1`,
|
|
57
|
+
the image running the app as the non-root `bun` user:
|
|
58
|
+
|
|
59
|
+
- `api`: the production dependencies, then `src/server.ts` run as it is.
|
|
60
|
+
Bun runs TypeScript, so there is no build stage.
|
|
61
|
+
- `react-router`: the production dependencies, then `bun run build`, then
|
|
62
|
+
`bun build/server/index.js`.
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
cd my-api
|
|
66
|
+
docker build -t my-api .
|
|
67
|
+
docker run -p 3000:3000 -e API_KEY=change-me my-api
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
cd my-site
|
|
72
|
+
docker build -t my-site .
|
|
73
|
+
docker run -p 3000:3000 my-site
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Commit the `bun.lock` that `bun install` wrote: the image installs from it
|
|
77
|
+
with `--frozen-lockfile`. The
|
|
78
|
+
[guide](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md#docker)
|
|
79
|
+
has the stages.
|
|
80
|
+
|
|
54
81
|
## Options
|
|
55
82
|
|
|
56
83
|
| option | |
|
|
@@ -65,9 +92,13 @@ or CI.
|
|
|
65
92
|
|
|
66
93
|
## Versions
|
|
67
94
|
|
|
68
|
-
- **alxia's packages**
|
|
69
|
-
|
|
70
|
-
|
|
95
|
+
- **alxia's packages** — `@alxia/core`, `@alxia/client`,
|
|
96
|
+
`@alxia/react-router` — are moved to the newest version on the registry
|
|
97
|
+
within the ranges this release of `@alxia/create` was published with:
|
|
98
|
+
`^0.3.1` writes `^0.3.4` once 0.3.4 is out, never `^0.4.0`. Just after a
|
|
99
|
+
release, while the registry does not serve that version yet, the newest of
|
|
100
|
+
the same minor is written (`^0.3.0`, which takes 0.3.1 once it arrives),
|
|
101
|
+
and the output says so. `bunx @alxia/create@<version>` picks an older set.
|
|
71
102
|
- **Everything else** — Zod, TypeScript, Vite, React, React Router, Tailwind
|
|
72
103
|
— is moved to the newest version on the registry when the project is
|
|
73
104
|
written, within the range alxia's packages accept: TypeScript within
|
|
@@ -96,6 +127,6 @@ the project it writes installs with `bun install`.
|
|
|
96
127
|
|
|
97
128
|
## Documentation
|
|
98
129
|
|
|
99
|
-
- [Guide](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md): each template file by file, what the `react-router` template
|
|
130
|
+
- [Guide](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md): each template file by file, what the `react-router` template adds to React Router's, each `Dockerfile`, how versions are chosen, and running it in CI.
|
|
100
131
|
- [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/create/docs/troubleshooting.md): each message the command prints, and what to do about it.
|
|
101
132
|
- [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/create/docs/roadmap.md): what is coming, and what is not planned.
|
package/dist/copy.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Template } from './args';
|
|
2
|
+
import type { Manifest } from './registry';
|
|
3
|
+
import type { AlxiaPackage } from './versions';
|
|
4
|
+
/**
|
|
5
|
+
* Where the templates are, beside `src/` here and beside `dist/` published:
|
|
6
|
+
* this module is bundled into `dist/index.js`, as `versions.ts` reads
|
|
7
|
+
* `../package.json`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TEMPLATES: string;
|
|
10
|
+
/**
|
|
11
|
+
* Files a template stores under another name, because a published tarball
|
|
12
|
+
* would not hold them: `bun publish`, as `npm publish`, leaves every
|
|
13
|
+
* `.gitignore` out, and Bun leaves `bunfig.toml` out too.
|
|
14
|
+
*/
|
|
15
|
+
export declare const RENAMED: Readonly<Record<string, string>>;
|
|
16
|
+
/**
|
|
17
|
+
* `template`, from `root`: its manifest, named `name` with alxia's packages
|
|
18
|
+
* at `alxia`'s ranges, and every other file, by the path it is written at.
|
|
19
|
+
*/
|
|
20
|
+
export declare function copyTemplate(template: Template, name: string, alxia: Record<AlxiaPackage, string>, root?: string): Promise<{
|
|
21
|
+
manifest: Manifest;
|
|
22
|
+
files: Record<string, Blob>;
|
|
23
|
+
}>;
|
|
24
|
+
//# sourceMappingURL=copy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copy.d.ts","sourceRoot":"","sources":["../src/copy.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,SAAS,QAErB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGpD,CAAC;AAEF;;;GAGG;AACH,wBAAsB,YAAY,CACjC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,EACnC,IAAI,GAAE,MAAkB,GACtB,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;CAAE,CAAC,CA4B9D"}
|
package/dist/index.js
CHANGED
|
@@ -78,9 +78,6 @@ async function clean(target, existed) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// src/templates/react-router.ts
|
|
82
|
-
import { join as join2 } from "node:path";
|
|
83
|
-
|
|
84
81
|
// src/versions.ts
|
|
85
82
|
import { dirname } from "node:path";
|
|
86
83
|
var ALXIA_PACKAGES = [
|
|
@@ -94,12 +91,6 @@ var PEER_RANGES = {
|
|
|
94
91
|
"react-router": "^8.0.0",
|
|
95
92
|
vite: "^7.0.0 || ^8.0.0"
|
|
96
93
|
};
|
|
97
|
-
function highestMajor(range) {
|
|
98
|
-
const majors = [...range.matchAll(/(?:^|[\s|^~>=])(\d+)/g)].map((match) => Number(match[1]));
|
|
99
|
-
if (majors.length === 0)
|
|
100
|
-
throw new Error(`no major in range ${range}`);
|
|
101
|
-
return Math.max(...majors);
|
|
102
|
-
}
|
|
103
94
|
async function alxiaRanges(manifestUrl = new URL("../package.json", import.meta.url)) {
|
|
104
95
|
const manifest = await Bun.file(manifestUrl).json();
|
|
105
96
|
const declared = manifest.devDependencies ?? {};
|
|
@@ -120,84 +111,43 @@ async function workspaceRange(name, range, from) {
|
|
|
120
111
|
return prefix === "*" ? version : `${prefix}${version}`;
|
|
121
112
|
}
|
|
122
113
|
|
|
123
|
-
// src/
|
|
124
|
-
|
|
125
|
-
return [
|
|
126
|
-
`create-react-router@${highestMajor(PEER_RANGES["react-router"])}`,
|
|
127
|
-
dir,
|
|
128
|
-
"--yes",
|
|
129
|
-
"--no-install",
|
|
130
|
-
"--no-git-init",
|
|
131
|
-
"--no-agent-skills",
|
|
132
|
-
"--no-motion"
|
|
133
|
-
];
|
|
134
|
-
}
|
|
114
|
+
// src/write.ts
|
|
115
|
+
import { join as join3 } from "node:path";
|
|
135
116
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
117
|
+
// src/copy.ts
|
|
118
|
+
import { join as join2 } from "node:path";
|
|
119
|
+
import { fileURLToPath } from "node:url";
|
|
120
|
+
var TEMPLATES2 = fileURLToPath(new URL("../templates", import.meta.url));
|
|
121
|
+
var RENAMED = {
|
|
122
|
+
gitignore: ".gitignore",
|
|
123
|
+
"_bunfig.toml": "bunfig.toml"
|
|
124
|
+
};
|
|
125
|
+
async function copyTemplate(template, name, alxia, root = TEMPLATES2) {
|
|
126
|
+
const dir = join2(root, template);
|
|
127
|
+
const stored = await Bun.file(join2(dir, "package.json")).json();
|
|
128
|
+
const manifest = { ...stored, name };
|
|
129
|
+
for (const field of ["dependencies", "devDependencies"]) {
|
|
130
|
+
const deps = stored[field];
|
|
131
|
+
if (deps === undefined)
|
|
132
|
+
continue;
|
|
133
|
+
manifest[field] = Object.fromEntries(Object.entries(deps).map(([pkg, range]) => [
|
|
134
|
+
pkg,
|
|
135
|
+
Object.hasOwn(alxia, pkg) ? alxia[pkg] : range
|
|
136
|
+
]));
|
|
156
137
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const scripts = manifest["scripts"];
|
|
161
|
-
if (scripts?.["dev"] !== "react-router dev" || scripts["build"] !== "react-router build" || scripts["start"] === undefined || manifest.dependencies?.["react-router"] === undefined) {
|
|
162
|
-
throw new ScaffoldChanged("package.json", "react-router in its dependencies, and the scripts `dev: react-router dev`, `build: react-router build` and a `start`");
|
|
138
|
+
const left = JSON.stringify(manifest).match(/"([^"]+)":"workspace:/);
|
|
139
|
+
if (left) {
|
|
140
|
+
throw new Error(`the ${template} template names ${left[1]} at workspace:, which this @alxia/create has no version for`);
|
|
163
141
|
}
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
...manifest,
|
|
171
|
-
scripts: { ...scripts, start: "bun build/server/index.js" },
|
|
172
|
-
dependencies
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
function sorted(deps) {
|
|
176
|
-
return Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a < b ? -1 : 1));
|
|
177
|
-
}
|
|
178
|
-
async function alxiaLayer(dir, alxia) {
|
|
179
|
-
const read = async (file) => {
|
|
180
|
-
const handle = Bun.file(join2(dir, file));
|
|
181
|
-
if (!await handle.exists()) {
|
|
182
|
-
throw new ScaffoldChanged(file, "it is missing");
|
|
183
|
-
}
|
|
184
|
-
return handle.text();
|
|
185
|
-
};
|
|
186
|
-
const manifest = addAlxia(JSON.parse(await read("package.json")), alxia);
|
|
187
|
-
const config = addPlugin(await read("vite.config.ts"));
|
|
188
|
-
if (await Bun.file(join2(dir, "bunfig.toml")).exists()) {
|
|
189
|
-
throw new ScaffoldChanged("bunfig.toml", "none, and there is one");
|
|
142
|
+
const files = {};
|
|
143
|
+
for await (const path of new Bun.Glob("**").scan({ cwd: dir, dot: true })) {
|
|
144
|
+
if (path === "package.json" || path.endsWith(".DS_Store"))
|
|
145
|
+
continue;
|
|
146
|
+
files[RENAMED[path] ?? path] = Bun.file(join2(dir, path));
|
|
190
147
|
}
|
|
191
|
-
return {
|
|
192
|
-
manifest,
|
|
193
|
-
files: { "vite.config.ts": config, "bunfig.toml": BUNFIG }
|
|
194
|
-
};
|
|
148
|
+
return { manifest, files };
|
|
195
149
|
}
|
|
196
150
|
|
|
197
|
-
// src/write.ts
|
|
198
|
-
import { mkdir } from "node:fs/promises";
|
|
199
|
-
import { basename as basename2, dirname as dirname2, join as join3 } from "node:path";
|
|
200
|
-
|
|
201
151
|
// src/registry.ts
|
|
202
152
|
function registryUrl(env) {
|
|
203
153
|
const url = env["BUN_CONFIG_REGISTRY"] || env["npm_config_registry"] || "https://registry.npmjs.org";
|
|
@@ -227,9 +177,19 @@ function choose(versions, range, latest) {
|
|
|
227
177
|
return newestWithin(versions, range);
|
|
228
178
|
return latest && versions.includes(latest) ? latest : newestWithin(versions, "*");
|
|
229
179
|
}
|
|
230
|
-
|
|
180
|
+
function sameMinor(range) {
|
|
181
|
+
const match = /(\d+)\.(\d+)\.\d+/.exec(range);
|
|
182
|
+
return match ? `~${match[1]}.${match[2]}.0` : undefined;
|
|
183
|
+
}
|
|
184
|
+
function newestOfMinor(versions, range) {
|
|
185
|
+
const minor = sameMinor(range);
|
|
186
|
+
const start = /\d+\.\d+\.\d+/.exec(range)?.[0];
|
|
187
|
+
const version = minor && newestWithin(versions, minor);
|
|
188
|
+
return version && start && Bun.semver.satisfies(start, `^${version}`) ? version : undefined;
|
|
189
|
+
}
|
|
190
|
+
async function bumpDependencies(manifest, registry, built = {}) {
|
|
231
191
|
const fields = ["dependencies", "devDependencies"];
|
|
232
|
-
const names = fields.flatMap((field) => Object.keys(manifest[field] ?? {})
|
|
192
|
+
const names = fields.flatMap((field) => Object.keys(manifest[field] ?? {}));
|
|
233
193
|
const fetched = new Map(await Promise.all(names.map(async (name) => [
|
|
234
194
|
name,
|
|
235
195
|
await packument(name, registry).catch(() => {
|
|
@@ -240,6 +200,7 @@ async function bumpDependencies(manifest, registry, keep = new Set) {
|
|
|
240
200
|
const held = [];
|
|
241
201
|
const failed = [];
|
|
242
202
|
const unmatched = [];
|
|
203
|
+
const behind = [];
|
|
243
204
|
const chosen = new Map;
|
|
244
205
|
const ordered = [...names].sort((a, b) => Number(b === "react-router") - Number(a === "react-router"));
|
|
245
206
|
for (const name of ordered) {
|
|
@@ -254,10 +215,17 @@ async function bumpDependencies(manifest, registry, keep = new Set) {
|
|
|
254
215
|
continue;
|
|
255
216
|
}
|
|
256
217
|
const versions = Object.keys(meta.versions);
|
|
257
|
-
const
|
|
218
|
+
const pinned = built[name];
|
|
219
|
+
const range = pinned ?? allowedRange(name);
|
|
258
220
|
const latest = meta["dist-tags"]?.["latest"];
|
|
259
221
|
const follow = chosen.get("react-router");
|
|
260
|
-
|
|
222
|
+
let version = name.startsWith("@react-router/") && follow && versions.includes(follow) ? follow : choose(versions, range, latest);
|
|
223
|
+
if (version === undefined && pinned !== undefined) {
|
|
224
|
+
version = newestOfMinor(versions, pinned);
|
|
225
|
+
if (version !== undefined) {
|
|
226
|
+
behind.push(`${name}: the registry has no release within ${range} yet; wrote ^${version}, the newest of ${sameMinor(pinned)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
261
229
|
if (version === undefined) {
|
|
262
230
|
unmatched.push(`${name}: no release within ${range ?? "any range"}; kept ${current}`);
|
|
263
231
|
continue;
|
|
@@ -271,214 +239,26 @@ async function bumpDependencies(manifest, registry, keep = new Set) {
|
|
|
271
239
|
moved.push(`${name} ${current} -> ${next}`);
|
|
272
240
|
deps[name] = next;
|
|
273
241
|
}
|
|
274
|
-
return { moved, held, failed, unmatched };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// src/templates/api.ts
|
|
278
|
-
function apiManifest(name, alxia) {
|
|
279
|
-
return {
|
|
280
|
-
name,
|
|
281
|
-
private: true,
|
|
282
|
-
type: "module",
|
|
283
|
-
scripts: {
|
|
284
|
-
dev: "bun --watch src/server.ts",
|
|
285
|
-
build: "bun build src/server.ts --target=bun --outdir=dist",
|
|
286
|
-
start: "bun dist/server.js",
|
|
287
|
-
test: "bun test",
|
|
288
|
-
typecheck: "tsc --noEmit"
|
|
289
|
-
},
|
|
290
|
-
dependencies: {
|
|
291
|
-
"@alxia/core": alxia["@alxia/core"],
|
|
292
|
-
zod: "^4.2.0"
|
|
293
|
-
},
|
|
294
|
-
devDependencies: {
|
|
295
|
-
"@alxia/client": alxia["@alxia/client"],
|
|
296
|
-
"@types/bun": "^1.4.2",
|
|
297
|
-
typescript: "^6.0.3"
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
var APP = `import { alxia, defineHook } from '@alxia/core';
|
|
302
|
-
import { z } from 'zod';
|
|
303
|
-
|
|
304
|
-
const Todo = z.object({ id: z.number(), title: z.string(), done: z.boolean() });
|
|
305
|
-
const NewTodo = z.object({ title: z.string().min(1) });
|
|
306
|
-
|
|
307
|
-
/** Set API_KEY in the environment: this default is for development. */
|
|
308
|
-
export const apiKey = Bun.env['API_KEY'] ?? 'dev-key';
|
|
309
|
-
|
|
310
|
-
// A hook of the routes it is given to: it answers 401 without the key, and
|
|
311
|
-
// that 401 joins the type of each, so the client reads it.
|
|
312
|
-
const requireKey = defineHook(({ request, reply }) =>
|
|
313
|
-
request.headers.get('x-api-key') === apiKey
|
|
314
|
-
? undefined
|
|
315
|
-
: reply(401, { error: 'unauthorized' as const }),
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
const todos: z.infer<typeof Todo>[] = [];
|
|
319
|
-
|
|
320
|
-
export const app = alxia()
|
|
321
|
-
.decorate({ todos })
|
|
322
|
-
.post(
|
|
323
|
-
'/todos',
|
|
324
|
-
[requireKey],
|
|
325
|
-
{ body: NewTodo, response: { 201: Todo } },
|
|
326
|
-
({ body, todos, reply }) => {
|
|
327
|
-
const todo = { id: todos.length + 1, title: body.title, done: false };
|
|
328
|
-
todos.push(todo);
|
|
329
|
-
return reply.created(todo);
|
|
330
|
-
},
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
export type App = typeof app;
|
|
334
|
-
`;
|
|
335
|
-
var SERVER = `import { app } from './app';
|
|
336
|
-
|
|
337
|
-
const server = app.listen(Number(Bun.env['PORT'] ?? 3000));
|
|
338
|
-
console.log(\`listening on \${server.url}\`);
|
|
339
|
-
`;
|
|
340
|
-
var SPEC = `import { expect, test } from 'bun:test';
|
|
341
|
-
import { client } from '@alxia/client';
|
|
342
|
-
import { apiKey, app } from './app';
|
|
343
|
-
|
|
344
|
-
const json = { 'content-type': 'application/json', 'x-api-key': apiKey };
|
|
345
|
-
|
|
346
|
-
test('creates a todo from JSON', async () => {
|
|
347
|
-
const response = await app.request('/todos', {
|
|
348
|
-
method: 'POST',
|
|
349
|
-
headers: json,
|
|
350
|
-
body: JSON.stringify({ title: 'Write a route' }),
|
|
351
|
-
});
|
|
352
|
-
expect(response.status).toBe(201);
|
|
353
|
-
expect(await response.json()).toEqual({
|
|
354
|
-
id: expect.any(Number),
|
|
355
|
-
title: 'Write a route',
|
|
356
|
-
done: false,
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
test('refuses an empty title with a 400 naming it', async () => {
|
|
361
|
-
const response = await app.request('/todos', {
|
|
362
|
-
method: 'POST',
|
|
363
|
-
headers: json,
|
|
364
|
-
body: JSON.stringify({ title: '' }),
|
|
365
|
-
});
|
|
366
|
-
expect(response.status).toBe(400);
|
|
367
|
-
expect((await response.json()).issues[0].path).toEqual(['title']);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test('the typed client reads each status the route answers', async () => {
|
|
371
|
-
// Given the app itself, the client calls its fetch in process.
|
|
372
|
-
const api = client(app, { headers: { 'x-api-key': apiKey } });
|
|
373
|
-
const created = await api.post('/todos', { body: { title: 'Call it typed' } });
|
|
374
|
-
if (created.status !== 201) throw new Error(\`got \${created.status}\`);
|
|
375
|
-
expect(created.data.title).toBe('Call it typed'); // data is the Todo schema's type
|
|
376
|
-
|
|
377
|
-
const anonymous = await client(app).post('/todos', { body: { title: 'No key' } });
|
|
378
|
-
expect(anonymous.status).toBe(401);
|
|
379
|
-
if (anonymous.status === 401) expect(anonymous.data.error).toBe('unauthorized');
|
|
380
|
-
});
|
|
381
|
-
`;
|
|
382
|
-
var TSCONFIG = `{
|
|
383
|
-
"compilerOptions": {
|
|
384
|
-
"types": ["bun"],
|
|
385
|
-
"lib": ["ESNext", "DOM"],
|
|
386
|
-
"target": "ESNext",
|
|
387
|
-
"module": "ESNext",
|
|
388
|
-
"moduleDetection": "force",
|
|
389
|
-
"moduleResolution": "bundler",
|
|
390
|
-
"verbatimModuleSyntax": true,
|
|
391
|
-
"noEmit": true,
|
|
392
|
-
"skipLibCheck": true,
|
|
393
|
-
"resolveJsonModule": true,
|
|
394
|
-
"strict": true,
|
|
395
|
-
"noImplicitOverride": true,
|
|
396
|
-
"noImplicitReturns": true,
|
|
397
|
-
"noFallthroughCasesInSwitch": true,
|
|
398
|
-
"noUncheckedIndexedAccess": true,
|
|
399
|
-
"exactOptionalPropertyTypes": true,
|
|
400
|
-
"noPropertyAccessFromIndexSignature": true,
|
|
401
|
-
"noUnusedLocals": true,
|
|
402
|
-
"noUnusedParameters": true,
|
|
403
|
-
"forceConsistentCasingInFileNames": true
|
|
404
|
-
},
|
|
405
|
-
"include": ["src"]
|
|
406
|
-
}
|
|
407
|
-
`;
|
|
408
|
-
var GITIGNORE = `node_modules
|
|
409
|
-
dist
|
|
410
|
-
*.log
|
|
411
|
-
.DS_Store
|
|
412
|
-
.env
|
|
413
|
-
.env.local
|
|
414
|
-
`;
|
|
415
|
-
var readme = (name) => `# ${name}
|
|
416
|
-
|
|
417
|
-
An [alxia](https://github.com/softistx/alxia) app with
|
|
418
|
-
[Zod](https://zod.dev), made with \`bun create @alxia\`.
|
|
419
|
-
|
|
420
|
-
\`\`\`sh
|
|
421
|
-
bun dev # http://localhost:3000, restarted on every change
|
|
422
|
-
bun test # src/app.spec.ts: in process, and through the typed client
|
|
423
|
-
bun run typecheck
|
|
424
|
-
bun run build # dist/server.js, run with \`bun start\`
|
|
425
|
-
\`\`\`
|
|
426
|
-
|
|
427
|
-
\`\`\`sh
|
|
428
|
-
curl -X POST localhost:3000/todos \\
|
|
429
|
-
-H 'content-type: application/json' -H 'x-api-key: dev-key' \\
|
|
430
|
-
-d '{"title":"Write a route"}'
|
|
431
|
-
\`\`\`
|
|
432
|
-
|
|
433
|
-
- \`src/app.ts\`: the app. \`POST /todos\` validates its body with Zod, and
|
|
434
|
-
its own hook, \`requireKey\`, answers 401 without the \`x-api-key\` header.
|
|
435
|
-
Set \`API_KEY\` in the environment outside development.
|
|
436
|
-
- \`src/server.ts\`: listens on \`PORT\`, 3000 by default.
|
|
437
|
-
- \`src/app.spec.ts\`: \`app.request()\` and \`@alxia/client\`, no port.
|
|
438
|
-
|
|
439
|
-
Next: [Getting started](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/getting-started.md)
|
|
440
|
-
and the rest of [\`@alxia/core\`'s guide](https://github.com/softistx/alxia/tree/develop/packages/core/docs).
|
|
441
|
-
`;
|
|
442
|
-
function apiFiles(name) {
|
|
443
|
-
return {
|
|
444
|
-
"src/app.ts": APP,
|
|
445
|
-
"src/server.ts": SERVER,
|
|
446
|
-
"src/app.spec.ts": SPEC,
|
|
447
|
-
"tsconfig.json": TSCONFIG,
|
|
448
|
-
".gitignore": GITIGNORE,
|
|
449
|
-
"README.md": readme(name)
|
|
450
|
-
};
|
|
242
|
+
return { moved, held, failed, unmatched, behind };
|
|
451
243
|
}
|
|
452
244
|
|
|
453
245
|
// src/write.ts
|
|
454
246
|
async function write(target, template, alxia, io) {
|
|
455
|
-
|
|
456
|
-
let files;
|
|
457
|
-
if (template === "api") {
|
|
458
|
-
manifest = apiManifest(packageName(target), alxia);
|
|
459
|
-
files = apiFiles(packageName(target));
|
|
460
|
-
} else {
|
|
461
|
-
await mkdir(dirname2(target), { recursive: true });
|
|
462
|
-
const code = await io.run([process.execPath, "x", ...scaffoldCommand(basename2(target))], dirname2(target));
|
|
463
|
-
if (code !== 0) {
|
|
464
|
-
throw new Error(`create-react-router exited with ${code}.`);
|
|
465
|
-
}
|
|
466
|
-
({ manifest, files } = await alxiaLayer(target, alxia));
|
|
467
|
-
}
|
|
247
|
+
const { manifest, files } = await copyTemplate(template, packageName(target), alxia);
|
|
468
248
|
io.out("Resolving the newest versions alxia accepts...");
|
|
469
|
-
const bumped = await bumpDependencies(manifest, { url: registryUrl(io.env), ...io.fetch ? { fetch: io.fetch } : {} },
|
|
249
|
+
const bumped = await bumpDependencies(manifest, { url: registryUrl(io.env), ...io.fetch ? { fetch: io.fetch } : {} }, alxia);
|
|
470
250
|
for (const line of bumped.moved)
|
|
471
251
|
io.out(` ${line}`);
|
|
472
|
-
for (const line of [...bumped.held, ...bumped.unmatched])
|
|
252
|
+
for (const line of [...bumped.held, ...bumped.unmatched, ...bumped.behind])
|
|
473
253
|
io.out(` ${line}`);
|
|
474
254
|
if (bumped.failed.length > 0) {
|
|
475
255
|
io.err(`${NAME}: warning: the registry did not answer for ${bumped.failed.join(", ")}; ` + "kept the versions the template ships.");
|
|
476
256
|
}
|
|
477
|
-
files["package.json"] = `${JSON.stringify(manifest, null, 2)}
|
|
478
|
-
`;
|
|
479
257
|
for (const [file, content] of Object.entries(files)) {
|
|
480
258
|
await Bun.write(join3(target, file), content);
|
|
481
259
|
}
|
|
260
|
+
await Bun.write(join3(target, "package.json"), `${JSON.stringify(manifest, null, 2)}
|
|
261
|
+
`);
|
|
482
262
|
return true;
|
|
483
263
|
}
|
|
484
264
|
// src/io.ts
|
|
@@ -553,7 +333,7 @@ ${USAGE}`);
|
|
|
553
333
|
}) !== undefined;
|
|
554
334
|
const made = await alxiaRanges().then((alxia) => write(target, template, alxia, io)).catch(async (error) => {
|
|
555
335
|
await clean(target, existed);
|
|
556
|
-
io.err(`${NAME}:
|
|
336
|
+
io.err(`${NAME}: failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
557
337
|
return false;
|
|
558
338
|
});
|
|
559
339
|
if (!made)
|
|
@@ -595,5 +375,5 @@ function askTemplate(io) {
|
|
|
595
375
|
// src/index.ts
|
|
596
376
|
process.exitCode = await main(process.argv.slice(2), process.cwd(), processIo());
|
|
597
377
|
|
|
598
|
-
//# debugId=
|
|
378
|
+
//# debugId=DC169542838EEB9F64756E2164756E21
|
|
599
379
|
//# sourceMappingURL=index.js.map
|