@veducio/keja 0.0.0-stage → 0.3.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.
Files changed (96) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +39 -2
  3. package/examples/starter/.env.example +6 -0
  4. package/examples/starter/keja.config.ts +7 -0
  5. package/examples/starter/keja.versioning.ts +5 -0
  6. package/examples/starter/migrations/0001_init.sql +1 -0
  7. package/examples/starter/package.json +14 -0
  8. package/examples/starter/tsconfig.json +9 -0
  9. package/package.json +62 -4
  10. package/src/access/cloudflare.test.ts +131 -0
  11. package/src/access/cloudflare.ts +193 -0
  12. package/src/argus/call.test.ts +98 -0
  13. package/src/argus/call.ts +153 -0
  14. package/src/argus/heartbeat.ts +65 -0
  15. package/src/argus/register.test.ts +120 -0
  16. package/src/argus/register.ts +145 -0
  17. package/src/cli/banner.test.ts +98 -0
  18. package/src/cli/banner.ts +209 -0
  19. package/src/cli/commands/build.ts +12 -0
  20. package/src/cli/commands/dev.ts +12 -0
  21. package/src/cli/commands/generate.ts +12 -0
  22. package/src/cli/commands/init.ts +57 -0
  23. package/src/cli/commands/migrate.ts +12 -0
  24. package/src/cli/commands/seed.ts +12 -0
  25. package/src/cli/commands/start.ts +12 -0
  26. package/src/cli/dev-reload.test.ts +87 -0
  27. package/src/cli/dev-reload.ts +182 -0
  28. package/src/cli/generate-project.ts +39 -0
  29. package/src/cli/index.ts +29 -0
  30. package/src/cli/init-project.test.ts +312 -0
  31. package/src/cli/init-project.ts +416 -0
  32. package/src/cli/migrate-project.ts +31 -0
  33. package/src/cli/run-project.test.ts +50 -0
  34. package/src/cli/run-project.ts +93 -0
  35. package/src/cli/seed-project.ts +46 -0
  36. package/src/db/client.test.ts +49 -0
  37. package/src/db/client.ts +61 -0
  38. package/src/db/migrate.test.ts +141 -0
  39. package/src/db/migrate.ts +146 -0
  40. package/src/db/owned-sql.ts +202 -0
  41. package/src/db/postgres.ts +90 -0
  42. package/src/env/required-env.test.ts +64 -0
  43. package/src/env/required-env.ts +120 -0
  44. package/src/errors/framework-errors.ts +295 -0
  45. package/src/grant/grant.test.ts +130 -0
  46. package/src/grant/grant.ts +268 -0
  47. package/src/http/create-app.test.ts +340 -0
  48. package/src/http/create-app.ts +196 -0
  49. package/src/http/openapi.ts +111 -0
  50. package/src/process/slot.ts +18 -0
  51. package/src/project/config.test.ts +143 -0
  52. package/src/project/config.ts +450 -0
  53. package/src/project/load.test.ts +60 -0
  54. package/src/project/load.ts +158 -0
  55. package/src/project/reload-graph.test.ts +52 -0
  56. package/src/project/reload-graph.ts +175 -0
  57. package/src/repository/define.ts +123 -0
  58. package/src/repository/generate.test.ts +58 -0
  59. package/src/repository/generate.ts +117 -0
  60. package/src/repository/query.test.ts +99 -0
  61. package/src/repository/query.ts +287 -0
  62. package/src/repository/runtime.test.ts +154 -0
  63. package/src/repository/runtime.ts +434 -0
  64. package/src/repository/sql.test.ts +78 -0
  65. package/src/repository/sql.ts +282 -0
  66. package/src/route/define.test.ts +65 -0
  67. package/src/route/define.ts +307 -0
  68. package/src/route/middleware.test.ts +63 -0
  69. package/src/route/middleware.ts +144 -0
  70. package/src/seed/apply.test.ts +65 -0
  71. package/src/seed/apply.ts +11 -0
  72. package/src/seed/define.test.ts +51 -0
  73. package/src/seed/define.ts +55 -0
  74. package/src/seed/load.test.ts +54 -0
  75. package/src/seed/load.ts +67 -0
  76. package/src/seed/report.test.ts +16 -0
  77. package/src/seed/report.ts +15 -0
  78. package/src/self-test/checks.ts +88 -0
  79. package/src/self-test/run.test.ts +128 -0
  80. package/src/self-test/run.ts +105 -0
  81. package/src/server/identity.test.ts +57 -0
  82. package/src/server/identity.ts +116 -0
  83. package/src/server/index.test.ts +15 -0
  84. package/src/server/index.ts +171 -0
  85. package/src/server/swap.test.ts +89 -0
  86. package/src/server/swap.ts +29 -0
  87. package/src/typeid/id.test.ts +46 -0
  88. package/src/typeid/id.ts +136 -0
  89. package/src/typeid/owned.test.ts +73 -0
  90. package/src/typeid/owned.ts +193 -0
  91. package/src/vault/client.test.ts +128 -0
  92. package/src/vault/client.ts +282 -0
  93. package/src/versioning/define.test.ts +79 -0
  94. package/src/versioning/define.ts +173 -0
  95. package/src/versioning/select.test.ts +79 -0
  96. package/src/versioning/select.ts +62 -0
@@ -0,0 +1,312 @@
1
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { createProject, resolveInitAnswers, runInit } from "./init-project.ts";
6
+
7
+ const ask = async (label: string, fallback: string) => {
8
+ asked.push([label, fallback]);
9
+ return answers.shift() ?? "";
10
+ };
11
+
12
+ let asked: [string, string][] = [];
13
+ let answers: string[] = [];
14
+
15
+ describe("resolveInitAnswers", () => {
16
+ it("keeps flags and skips prompts", async () => {
17
+ asked = [];
18
+ await expect(
19
+ resolveInitAnswers(
20
+ {
21
+ name: "billing",
22
+ dir: "./apps/billing",
23
+ public: "./api/public",
24
+ service: "api/service",
25
+ },
26
+ ask,
27
+ ),
28
+ ).resolves.toEqual({
29
+ name: "billing",
30
+ directory: "./apps/billing",
31
+ publicDir: "api/public",
32
+ serviceDir: "api/service",
33
+ });
34
+ expect(asked).toEqual([]);
35
+ });
36
+
37
+ it("prompts for missing answers and uses the defaults", async () => {
38
+ asked = [];
39
+ answers = ["", "notes", "", ""];
40
+ await expect(resolveInitAnswers({}, ask)).resolves.toEqual({
41
+ name: "my-app",
42
+ directory: "notes",
43
+ publicDir: "src/public",
44
+ serviceDir: "src/service",
45
+ });
46
+ expect(asked.map((item) => item[1])).toEqual([
47
+ "my-app",
48
+ "./my-app",
49
+ "./src/public",
50
+ "./src/service",
51
+ ]);
52
+ });
53
+
54
+ it("rejects a name or route folder that cannot be created safely", async () => {
55
+ await expect(
56
+ resolveInitAnswers({ name: "../billing" }, ask),
57
+ ).rejects.toThrow(
58
+ "Project name must use letters, numbers, dots, hyphens, or underscores.",
59
+ );
60
+ await expect(
61
+ resolveInitAnswers({ name: "billing", public: "../secret" }, ask),
62
+ ).rejects.toThrow(
63
+ "Public server folder must be a path inside the project.",
64
+ );
65
+ });
66
+ });
67
+
68
+ describe("createProject", () => {
69
+ it("requires bun before it writes a project", async () => {
70
+ const root = mkdtempSync(path.join(tmpdir(), "keja-init-"));
71
+ const calls: string[] = [];
72
+ await expect(
73
+ createProject({
74
+ answers: {
75
+ name: "my-app",
76
+ directory: "./my-app",
77
+ publicDir: "src/public",
78
+ serviceDir: "src/service",
79
+ },
80
+ cwd: root,
81
+ templateDir: path.join(root, "template"),
82
+ packageRoot: path.join(root, "keja"),
83
+ run: async (command, args) => {
84
+ calls.push(`${command} ${args.join(" ")}`);
85
+ return command === "bun" ? 1 : 0;
86
+ },
87
+ }),
88
+ ).rejects.toThrow("Bun is required to create a project.");
89
+ expect(calls).toEqual(["bun --version"]);
90
+ expect(() =>
91
+ readFileSync(path.join(root, "my-app", "package.json")),
92
+ ).toThrow();
93
+ });
94
+
95
+ it("requires git before it writes a project", async () => {
96
+ const root = mkdtempSync(path.join(tmpdir(), "keja-init-"));
97
+ const calls: string[] = [];
98
+ await expect(
99
+ createProject({
100
+ answers: {
101
+ name: "my-app",
102
+ directory: "./my-app",
103
+ publicDir: "src/public",
104
+ serviceDir: "src/service",
105
+ },
106
+ cwd: root,
107
+ templateDir: path.join(root, "template"),
108
+ packageRoot: path.join(root, "keja"),
109
+ run: async (command, args) => {
110
+ calls.push(`${command} ${args.join(" ")}`);
111
+ return command === "git" ? 1 : 0;
112
+ },
113
+ }),
114
+ ).rejects.toThrow("Git is required to create a project.");
115
+ expect(calls).toEqual(["bun --version", "git --version"]);
116
+ });
117
+
118
+ it("copies the starter, rewrites the project files, and initializes git", async () => {
119
+ const root = mkdtempSync(path.join(tmpdir(), "keja-init-"));
120
+ const templateDir = path.join(root, "template");
121
+ const packageRoot = path.join(root, "keja");
122
+ mkdirSync(templateDir, { recursive: true });
123
+ mkdirSync(packageRoot, { recursive: true });
124
+ mkdirSync(path.join(templateDir, "node_modules", "left-behind"), {
125
+ recursive: true,
126
+ });
127
+ writeFileSync(
128
+ path.join(templateDir, "keja.versioning.ts"),
129
+ "export default 1;\n",
130
+ );
131
+ writeFileSync(path.join(templateDir, ".env.example"), "HOST=127.0.0.1\n");
132
+ writeFileSync(path.join(templateDir, "package.json"), "{}\n");
133
+ writeFileSync(
134
+ path.join(packageRoot, "package.json"),
135
+ JSON.stringify({ version: "0.1.0" }),
136
+ );
137
+ writeFileSync(
138
+ path.join(templateDir, "node_modules", "left-behind", "skip.txt"),
139
+ "nope",
140
+ );
141
+ const calls: string[] = [];
142
+ const directory = await createProject({
143
+ answers: {
144
+ name: "notes",
145
+ directory: "./notes",
146
+ publicDir: "routes/public",
147
+ serviceDir: "routes/service",
148
+ },
149
+ cwd: root,
150
+ templateDir,
151
+ packageRoot,
152
+ run: async (command, args, cwd) => {
153
+ calls.push(`${command} ${args.join(" ")} ${cwd}`);
154
+ return 0;
155
+ },
156
+ });
157
+
158
+ expect(directory).toBe(path.resolve(root, "notes"));
159
+ expect(calls.map((call) => call.slice(0, call.lastIndexOf(" ")))).toEqual([
160
+ "bun --version",
161
+ "git --version",
162
+ "bun install",
163
+ "git init",
164
+ ]);
165
+ expect(calls[2]).toBe(`bun install ${directory}`);
166
+ expect(calls[3]).toBe(`git init ${directory}`);
167
+ expect(
168
+ readFileSync(path.join(directory, "keja.versioning.ts"), "utf8"),
169
+ ).toBe("export default 1;\n");
170
+ expect(readFileSync(path.join(directory, ".env"), "utf8")).toBe(
171
+ "HOST=127.0.0.1\n",
172
+ );
173
+ expect(readFileSync(path.join(directory, ".env.example"), "utf8")).toBe(
174
+ "HOST=127.0.0.1\n",
175
+ );
176
+ expect(
177
+ readFileSync(path.join(directory, "package.json"), "utf8"),
178
+ ).toContain('"name": "notes"');
179
+ expect(
180
+ readFileSync(path.join(directory, "package.json"), "utf8"),
181
+ ).toContain('"@veducio/keja": "^0.1.0"');
182
+ expect(
183
+ readFileSync(path.join(directory, "package.json"), "utf8"),
184
+ ).toContain('"dev": "keja dev"');
185
+ expect(readFileSync(path.join(directory, ".npmrc"), "utf8")).toBe(
186
+ "@veducio:registry=https://npm.pkg.github.com\n",
187
+ );
188
+ expect(
189
+ readFileSync(path.join(directory, "routes/public/hello.ts"), "utf8"),
190
+ ).toContain('from "@veducio/keja"');
191
+ expect(
192
+ readFileSync(path.join(directory, "keja.config.ts"), "utf8"),
193
+ ).toContain('public_path: "routes/public"');
194
+ expect(
195
+ readFileSync(path.join(directory, "keja.config.ts"), "utf8"),
196
+ ).toContain('service_path: "routes/service"');
197
+ expect(
198
+ readFileSync(path.join(directory, "tsconfig.json"), "utf8"),
199
+ ).not.toContain("paths");
200
+ expect(
201
+ readFileSync(path.join(directory, "routes/public/hello.ts"), "utf8"),
202
+ ).toContain('path: "/hello"');
203
+ expect(
204
+ readFileSync(
205
+ path.join(directory, "routes/public/_middleware.ts"),
206
+ "utf8",
207
+ ),
208
+ ).toContain("defineMiddleware");
209
+ expect(
210
+ readFileSync(path.join(directory, "routes/service/ping.ts"), "utf8"),
211
+ ).toContain('method: "post"');
212
+ expect(() =>
213
+ readFileSync(path.join(directory, "node_modules/left-behind/skip.txt")),
214
+ ).toThrow();
215
+ });
216
+
217
+ it("leaves npm as the registry for the public package", async () => {
218
+ const root = mkdtempSync(path.join(tmpdir(), "keja-init-"));
219
+ const templateDir = path.join(root, "template");
220
+ const packageRoot = path.join(root, "keja");
221
+ mkdirSync(templateDir, { recursive: true });
222
+ mkdirSync(packageRoot, { recursive: true });
223
+ writeFileSync(
224
+ path.join(packageRoot, "package.json"),
225
+ JSON.stringify({
226
+ name: "@veducio/keja",
227
+ version: "0.2.0",
228
+ publishConfig: {
229
+ registry: "https://registry.npmjs.org",
230
+ access: "public",
231
+ },
232
+ }),
233
+ );
234
+ const directory = await createProject({
235
+ answers: {
236
+ name: "notes",
237
+ directory: "./notes",
238
+ publicDir: "src/public",
239
+ serviceDir: "src/service",
240
+ },
241
+ cwd: root,
242
+ templateDir,
243
+ packageRoot,
244
+ run: async () => 0,
245
+ });
246
+ const source = readFileSync(path.join(directory, "package.json"), "utf8");
247
+ expect(source).toContain('"@veducio/keja": "^0.2.0"');
248
+ expect(
249
+ readFileSync(path.join(directory, "src/public/hello.ts"), "utf8"),
250
+ ).toContain('from "@veducio/keja"');
251
+ expect(
252
+ readFileSync(path.join(directory, "keja.config.ts"), "utf8"),
253
+ ).toContain('from "@veducio/keja"');
254
+ expect(() => readFileSync(path.join(directory, ".npmrc"))).toThrow();
255
+ });
256
+
257
+ it("stops when the destination already has files", async () => {
258
+ const root = mkdtempSync(path.join(tmpdir(), "keja-init-"));
259
+ mkdirSync(path.join(root, "notes"));
260
+ writeFileSync(path.join(root, "notes", "kept.txt"), "keep");
261
+ await expect(
262
+ createProject({
263
+ answers: {
264
+ name: "notes",
265
+ directory: "./notes",
266
+ publicDir: "src/public",
267
+ serviceDir: "src/service",
268
+ },
269
+ cwd: root,
270
+ templateDir: path.join(root, "template"),
271
+ packageRoot: path.join(root, "keja"),
272
+ run: async () => 0,
273
+ }),
274
+ ).rejects.toThrow("already exists and is not empty");
275
+ });
276
+ });
277
+
278
+ describe("runInit", () => {
279
+ it("uses defaults when the terminal is not interactive", async () => {
280
+ const root = mkdtempSync(path.join(tmpdir(), "keja-init-"));
281
+ const templateDir = path.join(root, "template");
282
+ const packageRoot = path.join(root, "keja");
283
+ mkdirSync(templateDir, { recursive: true });
284
+ mkdirSync(packageRoot, { recursive: true });
285
+ writeFileSync(
286
+ path.join(packageRoot, "package.json"),
287
+ JSON.stringify({ version: "0.1.0" }),
288
+ );
289
+ writeFileSync(
290
+ path.join(templateDir, "keja.versioning.ts"),
291
+ "export default 1;\n",
292
+ );
293
+ const lines: string[] = [];
294
+ const directory = await runInit({
295
+ input: {},
296
+ cwd: root,
297
+ templateDir,
298
+ packageRoot,
299
+ interactive: false,
300
+ ask,
301
+ run: async () => 0,
302
+ log(message) {
303
+ lines.push(message);
304
+ },
305
+ });
306
+ expect(directory).toBe(path.resolve(root, "my-app"));
307
+ expect(lines.join("\n")).toContain("Created my-app");
308
+ expect(
309
+ readFileSync(path.join(directory, "src/public/hello.ts"), "utf8"),
310
+ ).toContain('path: "/hello"');
311
+ });
312
+ });