@alxia/create 0.1.0
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/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/args.d.ts +19 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +599 -0
- package/dist/index.js.map +19 -0
- package/dist/io.d.ts +18 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/main.d.ts +9 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/registry.d.ts +44 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/target.d.ts +7 -0
- package/dist/target.d.ts.map +1 -0
- package/dist/templates/api.d.ts +16 -0
- package/dist/templates/api.d.ts.map +1 -0
- package/dist/templates/react-router.d.ts +26 -0
- package/dist/templates/react-router.d.ts.map +1 -0
- package/dist/versions.d.ts +27 -0
- package/dist/versions.d.ts.map +1 -0
- package/dist/write.d.ts +6 -0
- package/dist/write.d.ts.map +1 -0
- package/docs/README.md +12 -0
- package/docs/guide.md +253 -0
- package/docs/roadmap.md +39 -0
- package/docs/troubleshooting.md +278 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/main.ts
|
|
5
|
+
import { stat as stat2 } from "node:fs/promises";
|
|
6
|
+
import { relative, resolve } from "node:path";
|
|
7
|
+
|
|
8
|
+
// src/args.ts
|
|
9
|
+
var NAME = "create-alxia";
|
|
10
|
+
var TEMPLATES = ["api", "react-router"];
|
|
11
|
+
function isTemplate(value) {
|
|
12
|
+
return TEMPLATES.includes(value);
|
|
13
|
+
}
|
|
14
|
+
function parseArgs(args) {
|
|
15
|
+
let dir;
|
|
16
|
+
let template;
|
|
17
|
+
let install = true;
|
|
18
|
+
let help = false;
|
|
19
|
+
for (let i = 0;i < args.length; i++) {
|
|
20
|
+
const arg = args[i];
|
|
21
|
+
if (arg === "--help" || arg === "-h")
|
|
22
|
+
help = true;
|
|
23
|
+
else if (arg === "--no-install")
|
|
24
|
+
install = false;
|
|
25
|
+
else if (arg === "--template" || arg === "-t" || arg.startsWith("--template=")) {
|
|
26
|
+
const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : args[++i];
|
|
27
|
+
if (value === undefined || value === "") {
|
|
28
|
+
return { error: `${arg} needs a template: ${TEMPLATES.join(" or ")}.` };
|
|
29
|
+
}
|
|
30
|
+
if (!isTemplate(value)) {
|
|
31
|
+
return {
|
|
32
|
+
error: `unknown template ${value}: use ${TEMPLATES.join(" or ")}.`
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
template = value;
|
|
36
|
+
} else if (arg.startsWith("-"))
|
|
37
|
+
return { error: `unknown option ${arg}.` };
|
|
38
|
+
else if (dir === undefined)
|
|
39
|
+
dir = arg;
|
|
40
|
+
else
|
|
41
|
+
return { error: `one directory only, given ${dir} and ${arg}.` };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
...dir === undefined ? {} : { dir },
|
|
45
|
+
...template === undefined ? {} : { template },
|
|
46
|
+
install,
|
|
47
|
+
help
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/target.ts
|
|
52
|
+
import { readdir, rm, stat } from "node:fs/promises";
|
|
53
|
+
import { basename, join } from "node:path";
|
|
54
|
+
function packageName(dir) {
|
|
55
|
+
const name = basename(dir).toLowerCase().replace(/[^a-z0-9._~-]+/g, "-").replace(/^[._-]+|-+$/g, "");
|
|
56
|
+
return name || "alxia-app";
|
|
57
|
+
}
|
|
58
|
+
async function refusal(target, shown = target) {
|
|
59
|
+
const info = await stat(target).catch(() => {
|
|
60
|
+
return;
|
|
61
|
+
});
|
|
62
|
+
if (!info)
|
|
63
|
+
return;
|
|
64
|
+
if (!info.isDirectory())
|
|
65
|
+
return `${shown} exists and is not a directory.`;
|
|
66
|
+
const entries = (await readdir(target)).sort();
|
|
67
|
+
if (entries.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
return `${shown} is not empty (${entries.slice(0, 3).join(", ")}${entries.length > 3 ? ", ..." : ""}), ` + "and create-alxia writes only into an empty directory. Choose another directory, or empty this one.";
|
|
70
|
+
}
|
|
71
|
+
async function clean(target, existed) {
|
|
72
|
+
if (!existed) {
|
|
73
|
+
await rm(target, { recursive: true, force: true });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const entry of await readdir(target).catch(() => [])) {
|
|
77
|
+
await rm(join(target, entry), { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/templates/react-router.ts
|
|
82
|
+
import { join as join2 } from "node:path";
|
|
83
|
+
|
|
84
|
+
// src/versions.ts
|
|
85
|
+
import { dirname } from "node:path";
|
|
86
|
+
var ALXIA_PACKAGES = [
|
|
87
|
+
"@alxia/client",
|
|
88
|
+
"@alxia/core",
|
|
89
|
+
"@alxia/react-router"
|
|
90
|
+
];
|
|
91
|
+
var PEER_RANGES = {
|
|
92
|
+
typescript: "^6.0.3 || ^7.0.0",
|
|
93
|
+
zod: "^4.2.0",
|
|
94
|
+
"react-router": "^8.0.0",
|
|
95
|
+
vite: "^7.0.0 || ^8.0.0"
|
|
96
|
+
};
|
|
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
|
+
async function alxiaRanges(manifestUrl = new URL("../package.json", import.meta.url)) {
|
|
104
|
+
const manifest = await Bun.file(manifestUrl).json();
|
|
105
|
+
const declared = manifest.devDependencies ?? {};
|
|
106
|
+
const ranges = {};
|
|
107
|
+
for (const name of ALXIA_PACKAGES) {
|
|
108
|
+
const range = declared[name];
|
|
109
|
+
if (range === undefined) {
|
|
110
|
+
throw new Error(`@alxia/create: its package.json names no ${name}`);
|
|
111
|
+
}
|
|
112
|
+
ranges[name] = range.startsWith("workspace:") ? await workspaceRange(name, range, dirname(manifestUrl.pathname)) : range;
|
|
113
|
+
}
|
|
114
|
+
return ranges;
|
|
115
|
+
}
|
|
116
|
+
async function workspaceRange(name, range, from) {
|
|
117
|
+
const file = Bun.resolveSync(`${name}/package.json`, from);
|
|
118
|
+
const { version } = await Bun.file(file).json();
|
|
119
|
+
const prefix = range.slice("workspace:".length);
|
|
120
|
+
return prefix === "*" ? version : `${prefix}${version}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/templates/react-router.ts
|
|
124
|
+
function scaffoldCommand(dir) {
|
|
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
|
+
}
|
|
135
|
+
|
|
136
|
+
class ScaffoldChanged extends Error {
|
|
137
|
+
constructor(file, expected) {
|
|
138
|
+
super(`create-react-router's ${file} is not what this @alxia/create expects: ${expected}. ` + "Nothing was kept. Try the newest @alxia/create, or add @alxia/react-router to a React Router app by hand, as its README shows.");
|
|
139
|
+
this.name = "ScaffoldChanged";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
var BUNFIG = `[run]
|
|
143
|
+
# The React Router CLI is a node script: this starts it on Bun, which
|
|
144
|
+
# alxia's server needs, even where a node is installed.
|
|
145
|
+
bun = true
|
|
146
|
+
`;
|
|
147
|
+
var IMPORT = `import { alxia } from "@alxia/react-router/vite";
|
|
148
|
+
`;
|
|
149
|
+
function addPlugin(config) {
|
|
150
|
+
const calls = config.match(/\breactRouter\(\)/g) ?? [];
|
|
151
|
+
if (!config.includes('from "@react-router/dev/vite"') || !/plugins:\s*\[[^\]]*\breactRouter\(\)[^\]]*\]/.test(config) || calls.length !== 1) {
|
|
152
|
+
throw new ScaffoldChanged("vite.config.ts", 'reactRouter() from "@react-router/dev/vite", called once in plugins: [...]');
|
|
153
|
+
}
|
|
154
|
+
if (config.includes("@alxia/react-router/vite")) {
|
|
155
|
+
throw new ScaffoldChanged("vite.config.ts", "it already imports @alxia/react-router/vite");
|
|
156
|
+
}
|
|
157
|
+
return IMPORT + config.replace(/\breactRouter\(\)/, "reactRouter(), alxia()");
|
|
158
|
+
}
|
|
159
|
+
function addAlxia(manifest, alxia) {
|
|
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`");
|
|
163
|
+
}
|
|
164
|
+
const dependencies = sorted({
|
|
165
|
+
...manifest.dependencies,
|
|
166
|
+
"@alxia/core": alxia["@alxia/core"],
|
|
167
|
+
"@alxia/react-router": alxia["@alxia/react-router"]
|
|
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");
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
manifest,
|
|
193
|
+
files: { "vite.config.ts": config, "bunfig.toml": BUNFIG }
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
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
|
+
// src/registry.ts
|
|
202
|
+
function registryUrl(env) {
|
|
203
|
+
const url = env["BUN_CONFIG_REGISTRY"] || env["npm_config_registry"] || "https://registry.npmjs.org";
|
|
204
|
+
return url.replace(/\/+$/, "");
|
|
205
|
+
}
|
|
206
|
+
function allowedRange(name) {
|
|
207
|
+
if (name === "react-router" || name.startsWith("@react-router/")) {
|
|
208
|
+
return PEER_RANGES["react-router"];
|
|
209
|
+
}
|
|
210
|
+
return name in PEER_RANGES ? PEER_RANGES[name] : undefined;
|
|
211
|
+
}
|
|
212
|
+
function newestWithin(versions, range) {
|
|
213
|
+
return versions.filter((v) => !v.includes("-") && Bun.semver.satisfies(v, range)).sort(Bun.semver.order).at(-1);
|
|
214
|
+
}
|
|
215
|
+
async function packument(name, registry) {
|
|
216
|
+
const call = registry.fetch ?? ((request) => fetch(request));
|
|
217
|
+
const response = await call(new Request(`${registry.url}/${name.replace("/", "%2f")}`, {
|
|
218
|
+
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
219
|
+
signal: AbortSignal.timeout(registry.timeout ?? 5000)
|
|
220
|
+
}));
|
|
221
|
+
if (!response.ok)
|
|
222
|
+
throw new Error(`${response.status}`);
|
|
223
|
+
return await response.json();
|
|
224
|
+
}
|
|
225
|
+
function choose(versions, range, latest) {
|
|
226
|
+
if (range !== undefined)
|
|
227
|
+
return newestWithin(versions, range);
|
|
228
|
+
return latest && versions.includes(latest) ? latest : newestWithin(versions, "*");
|
|
229
|
+
}
|
|
230
|
+
async function bumpDependencies(manifest, registry, keep = new Set) {
|
|
231
|
+
const fields = ["dependencies", "devDependencies"];
|
|
232
|
+
const names = fields.flatMap((field) => Object.keys(manifest[field] ?? {}).filter((name) => !keep.has(name)));
|
|
233
|
+
const fetched = new Map(await Promise.all(names.map(async (name) => [
|
|
234
|
+
name,
|
|
235
|
+
await packument(name, registry).catch(() => {
|
|
236
|
+
return;
|
|
237
|
+
})
|
|
238
|
+
])));
|
|
239
|
+
const moved = [];
|
|
240
|
+
const held = [];
|
|
241
|
+
const failed = [];
|
|
242
|
+
const unmatched = [];
|
|
243
|
+
const chosen = new Map;
|
|
244
|
+
const ordered = [...names].sort((a, b) => Number(b === "react-router") - Number(a === "react-router"));
|
|
245
|
+
for (const name of ordered) {
|
|
246
|
+
const field = fields.find((f) => manifest[f]?.[name] !== undefined);
|
|
247
|
+
const deps = field && manifest[field];
|
|
248
|
+
const current = deps?.[name];
|
|
249
|
+
if (!deps || current === undefined)
|
|
250
|
+
continue;
|
|
251
|
+
const meta = fetched.get(name);
|
|
252
|
+
if (!meta?.versions) {
|
|
253
|
+
failed.push(name);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const versions = Object.keys(meta.versions);
|
|
257
|
+
const range = allowedRange(name);
|
|
258
|
+
const latest = meta["dist-tags"]?.["latest"];
|
|
259
|
+
const follow = chosen.get("react-router");
|
|
260
|
+
const version = name.startsWith("@react-router/") && follow && versions.includes(follow) ? follow : choose(versions, range, latest);
|
|
261
|
+
if (version === undefined) {
|
|
262
|
+
unmatched.push(`${name}: no release within ${range ?? "any range"}; kept ${current}`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
chosen.set(name, version);
|
|
266
|
+
if (range !== undefined && latest && Bun.semver.order(latest, version) > 0 && !Bun.semver.satisfies(latest, range)) {
|
|
267
|
+
held.push(`${name}: kept to ${range}, where the newest is ${version}; npm's latest, ${latest}, is outside it`);
|
|
268
|
+
}
|
|
269
|
+
const next = `^${version}`;
|
|
270
|
+
if (next !== current)
|
|
271
|
+
moved.push(`${name} ${current} -> ${next}`);
|
|
272
|
+
deps[name] = next;
|
|
273
|
+
}
|
|
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
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/write.ts
|
|
454
|
+
async function write(target, template, alxia, io) {
|
|
455
|
+
let manifest;
|
|
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
|
+
}
|
|
468
|
+
io.out("Resolving the newest versions alxia accepts...");
|
|
469
|
+
const bumped = await bumpDependencies(manifest, { url: registryUrl(io.env), ...io.fetch ? { fetch: io.fetch } : {} }, new Set(Object.keys(alxia)));
|
|
470
|
+
for (const line of bumped.moved)
|
|
471
|
+
io.out(` ${line}`);
|
|
472
|
+
for (const line of [...bumped.held, ...bumped.unmatched])
|
|
473
|
+
io.out(` ${line}`);
|
|
474
|
+
if (bumped.failed.length > 0) {
|
|
475
|
+
io.err(`${NAME}: warning: the registry did not answer for ${bumped.failed.join(", ")}; ` + "kept the versions the template ships.");
|
|
476
|
+
}
|
|
477
|
+
files["package.json"] = `${JSON.stringify(manifest, null, 2)}
|
|
478
|
+
`;
|
|
479
|
+
for (const [file, content] of Object.entries(files)) {
|
|
480
|
+
await Bun.write(join3(target, file), content);
|
|
481
|
+
}
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
// src/io.ts
|
|
485
|
+
function processIo() {
|
|
486
|
+
return {
|
|
487
|
+
out: (line) => console.log(line),
|
|
488
|
+
err: (line) => console.error(line),
|
|
489
|
+
ask: (question, fallback) => {
|
|
490
|
+
if (!process.stdin.isTTY)
|
|
491
|
+
return;
|
|
492
|
+
const answer = prompt(question, fallback);
|
|
493
|
+
return answer === null ? null : answer.trim() || fallback;
|
|
494
|
+
},
|
|
495
|
+
run: async (command, cwd) => Bun.spawn([...command], { cwd, stdio: ["inherit", "inherit", "inherit"] }).exited,
|
|
496
|
+
env: process.env
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/main.ts
|
|
501
|
+
var USAGE = `Usage: bun create @alxia [dir] [options]
|
|
502
|
+
|
|
503
|
+
Writes a new alxia app in dir, which must be empty or not exist yet. Asks
|
|
504
|
+
for what is not given.
|
|
505
|
+
|
|
506
|
+
Options:
|
|
507
|
+
-t, --template <name> api an alxia app with Zod, a spec and the typed client
|
|
508
|
+
react-router React Router's official template, served by alxia
|
|
509
|
+
--no-install write the files, skip bun install
|
|
510
|
+
-h, --help show this help`;
|
|
511
|
+
async function main(args, cwd, io) {
|
|
512
|
+
const options = parseArgs(args);
|
|
513
|
+
if ("error" in options) {
|
|
514
|
+
io.err(`${NAME}: ${options.error}
|
|
515
|
+
|
|
516
|
+
${USAGE}`);
|
|
517
|
+
return 1;
|
|
518
|
+
}
|
|
519
|
+
if (options.help) {
|
|
520
|
+
io.out(USAGE);
|
|
521
|
+
return 0;
|
|
522
|
+
}
|
|
523
|
+
const dir = options.dir ?? io.ask("Where should the project go?", "alxia-app");
|
|
524
|
+
if (dir === null) {
|
|
525
|
+
io.err(`${NAME}: cancelled, nothing written.`);
|
|
526
|
+
return 1;
|
|
527
|
+
}
|
|
528
|
+
if (dir === undefined) {
|
|
529
|
+
io.err(`${NAME}: no directory given, and no terminal to ask in.
|
|
530
|
+
|
|
531
|
+
${USAGE}`);
|
|
532
|
+
return 1;
|
|
533
|
+
}
|
|
534
|
+
const template = options.template ?? askTemplate(io);
|
|
535
|
+
if (template === null) {
|
|
536
|
+
io.err(`${NAME}: cancelled, nothing written.`);
|
|
537
|
+
return 1;
|
|
538
|
+
}
|
|
539
|
+
if (typeof template === "object") {
|
|
540
|
+
io.err(`${NAME}: ${template.error}
|
|
541
|
+
|
|
542
|
+
${USAGE}`);
|
|
543
|
+
return 1;
|
|
544
|
+
}
|
|
545
|
+
const target = resolve(cwd, dir);
|
|
546
|
+
const refused = await refusal(target, dir);
|
|
547
|
+
if (refused) {
|
|
548
|
+
io.err(`${NAME}: ${refused}`);
|
|
549
|
+
return 1;
|
|
550
|
+
}
|
|
551
|
+
const existed = await stat2(target).catch(() => {
|
|
552
|
+
return;
|
|
553
|
+
}) !== undefined;
|
|
554
|
+
const made = await alxiaRanges().then((alxia) => write(target, template, alxia, io)).catch(async (error) => {
|
|
555
|
+
await clean(target, existed);
|
|
556
|
+
io.err(`${NAME}: ${error instanceof ScaffoldChanged ? "" : "failed: "}${error instanceof Error ? error.message : String(error)}`);
|
|
557
|
+
return false;
|
|
558
|
+
});
|
|
559
|
+
if (!made)
|
|
560
|
+
return 1;
|
|
561
|
+
let installed = true;
|
|
562
|
+
if (options.install) {
|
|
563
|
+
io.out(`
|
|
564
|
+
Installing dependencies with bun install...`);
|
|
565
|
+
installed = await io.run([process.execPath, "install"], target) === 0;
|
|
566
|
+
if (!installed)
|
|
567
|
+
io.err(`${NAME}: bun install failed; the files are written.`);
|
|
568
|
+
}
|
|
569
|
+
io.out(done(relative(cwd, target) || ".", template, options.install && installed));
|
|
570
|
+
return installed ? 0 : 1;
|
|
571
|
+
}
|
|
572
|
+
function done(where, template, installed) {
|
|
573
|
+
const steps = [
|
|
574
|
+
where === "." ? undefined : `cd ${where}`,
|
|
575
|
+
installed ? undefined : "bun install",
|
|
576
|
+
"bun dev"
|
|
577
|
+
].filter((step) => step !== undefined);
|
|
578
|
+
return `
|
|
579
|
+
Done: ${where === "." ? "this directory" : where} holds the ${template} template. Next:
|
|
580
|
+
|
|
581
|
+
${steps.map((step) => ` ${step}`).join(`
|
|
582
|
+
`)}
|
|
583
|
+
`;
|
|
584
|
+
}
|
|
585
|
+
function askTemplate(io) {
|
|
586
|
+
const answer = io.ask(`Which template? ${TEMPLATES.join(" or ")}`, "api");
|
|
587
|
+
if (answer === null)
|
|
588
|
+
return null;
|
|
589
|
+
if (answer === undefined) {
|
|
590
|
+
return { error: "no --template given, and no terminal to ask in." };
|
|
591
|
+
}
|
|
592
|
+
return isTemplate(answer) ? answer : { error: `unknown template ${answer}: use ${TEMPLATES.join(" or ")}.` };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/index.ts
|
|
596
|
+
process.exitCode = await main(process.argv.slice(2), process.cwd(), processIo());
|
|
597
|
+
|
|
598
|
+
//# debugId=8B48AE430B9AACCF64756E2164756E21
|
|
599
|
+
//# sourceMappingURL=index.js.map
|