@tatara/cli 0.7.0 → 0.9.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/dist/bin.mjs +395 -67
- package/package.json +1 -1
- package/templates/features/authz/files+drizzle/src/core/authz/index.ts +15 -0
- package/templates/features/authz/files+prisma/src/core/authz/index.ts +14 -0
- package/templates/versions.json +10 -10
- package/templates/features/authz/files/src/core/authz/commands/grant-role.ts +0 -21
- package/templates/features/authz/files/src/core/authz/commands/sync-permissions.ts +0 -11
- package/templates/features/authz/files/src/core/authz/db/seed.ts +0 -10
- package/templates/features/authz/files/src/core/authz/index.ts +0 -9
- package/templates/features/authz/files+drizzle/src/core/authz/db/store.ts +0 -275
- package/templates/features/authz/files+prisma/src/core/authz/db/store.ts +0 -236
- package/templates/features/db/files/src/core/db/commands/migrate.ts +0 -12
- package/templates/features/db/files/src/core/db/commands/reset.ts +0 -19
- package/templates/features/db/files/src/core/db/commands/seed.ts +0 -13
- package/templates/features/db/files/src/core/db/tests/reset.test.ts +0 -7
- package/templates/features/email/files/src/core/email/commands/send-test.ts +0 -13
- package/templates/features/jobs/files/src/core/jobs/commands/enqueue.ts +0 -13
- package/templates/features/storage/files/src/core/storage/commands/check.ts +0 -18
package/dist/bin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from "commander";
|
|
2
|
+
import { Command, Option } from "commander";
|
|
3
3
|
import pc from "picocolors";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
@@ -190,6 +190,18 @@ function ignored(v, text) {
|
|
|
190
190
|
return new RegExp(`tatara-check-ignore\\s+${v.rule}:\\s*\\S`).test(previous);
|
|
191
191
|
}
|
|
192
192
|
//#endregion
|
|
193
|
+
//#region src/project/ejected.ts
|
|
194
|
+
/** Packages the app ejected (`tatara eject`); their code lives in src/ejected/<name>. */
|
|
195
|
+
function ejectedPackages(config) {
|
|
196
|
+
return Object.keys(config.ejected ?? {});
|
|
197
|
+
}
|
|
198
|
+
/** Points `@tatara/<name>` and `@tatara/<name>/<entry>` imports at the app's ejected copy. */
|
|
199
|
+
function pointAtEjected(text, names) {
|
|
200
|
+
if (names.length === 0) return text;
|
|
201
|
+
const pattern = new RegExp(`(["'])@tatara/(${names.join("|")})(?:/([\\w-]+))?\\1`, "g");
|
|
202
|
+
return text.replace(pattern, (_match, quote, name, entry) => `${quote}#ejected/${name}/${entry ?? "index"}${quote}`);
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
193
205
|
//#region src/commands/sync.ts
|
|
194
206
|
var sync_exports = /* @__PURE__ */ __exportAll({
|
|
195
207
|
specifier: () => specifier,
|
|
@@ -227,6 +239,55 @@ ${imports.join("\n")}${imports.length ? "\n" : ""}
|
|
|
227
239
|
export const ${options.exportName}: ${options.typeName}[] = [${names.join(", ")}];
|
|
228
240
|
`;
|
|
229
241
|
}
|
|
242
|
+
/** What the app's installed (or ejected) @tatara/* packages contribute, by package name. */
|
|
243
|
+
async function contributions(project, kind) {
|
|
244
|
+
const pkg = JSON.parse(await readFile(join(project.root, "package.json"), "utf8"));
|
|
245
|
+
const ejected = ejectedPackages(project.config);
|
|
246
|
+
const names = [...Object.keys({
|
|
247
|
+
...pkg.dependencies,
|
|
248
|
+
...pkg.devDependencies
|
|
249
|
+
}).filter((n) => n.startsWith("@tatara/")).map((n) => n.slice(8)), ...ejected];
|
|
250
|
+
const out = [];
|
|
251
|
+
for (const name of [...new Set(names)].sort()) {
|
|
252
|
+
const file = ejected.includes(name) ? join(project.root, "src/ejected", name, "tatara-package.json") : join(project.root, "node_modules/@tatara", name, "package.json");
|
|
253
|
+
if (!existsSync(file)) continue;
|
|
254
|
+
const json = JSON.parse(await readFile(file, "utf8"));
|
|
255
|
+
out.push(...(ejected.includes(name) ? json[kind] : json.tatara?.[kind]) ?? []);
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Like listModule, plus the packages' factories: `own` are the app's files, and an own entry wins
|
|
261
|
+
* over a package's entry with the same name.
|
|
262
|
+
*/
|
|
263
|
+
function listWithPackages(files, from, options) {
|
|
264
|
+
if (from.length === 0) return listModule(files, options);
|
|
265
|
+
const imports = /* @__PURE__ */ new Map();
|
|
266
|
+
const bind = (name, spec) => {
|
|
267
|
+
const existing = imports.get(name);
|
|
268
|
+
if (existing && existing !== spec) throw new Error(`tatara packages need "${name}" from both ${existing} and ${spec}`);
|
|
269
|
+
imports.set(name, spec);
|
|
270
|
+
};
|
|
271
|
+
for (const c of from) {
|
|
272
|
+
bind(c.factory, c.import);
|
|
273
|
+
for (const [arg, spec] of Object.entries(c.args)) bind(arg, spec);
|
|
274
|
+
}
|
|
275
|
+
const named = [...imports].map(([name, spec]) => `import { ${name} } from "${spec}";`);
|
|
276
|
+
const own = files.map((f) => `import ${identifier(f, options.prefix)} from "${specifier(f)}";`);
|
|
277
|
+
const calls = from.map((c) => `...${c.factory}({ ${Object.keys(c.args).join(", ")} })`);
|
|
278
|
+
const t = options.typeName;
|
|
279
|
+
return `${HEADER}${options.typeImport}
|
|
280
|
+
${[...named, ...own].join("\n")}
|
|
281
|
+
|
|
282
|
+
const own: ${t}[] = [${files.map((f) => identifier(f, options.prefix)).join(", ")}];
|
|
283
|
+
// from the installed @tatara/* packages; the app's own entry of the same name wins
|
|
284
|
+
const fromPackages: ${t}[] = [${calls.join(", ")}];
|
|
285
|
+
export const ${options.exportName}: ${t}[] = [
|
|
286
|
+
...own,
|
|
287
|
+
...fromPackages.filter((c) => !own.some((o) => o.name === c.name)),
|
|
288
|
+
];
|
|
289
|
+
`;
|
|
290
|
+
}
|
|
230
291
|
async function writeIfChanged(path, content, written) {
|
|
231
292
|
if ((existsSync(path) ? await readFile(path, "utf8") : void 0) !== content) {
|
|
232
293
|
await writeFile(path, content);
|
|
@@ -235,19 +296,21 @@ async function writeIfChanged(path, content, written) {
|
|
|
235
296
|
}
|
|
236
297
|
async function sync(project = loadProject()) {
|
|
237
298
|
const { root } = project;
|
|
299
|
+
const ejected = ejectedPackages(project.config);
|
|
300
|
+
const write = (path, content, out) => writeIfChanged(path, pointAtEjected(content, ejected), out);
|
|
238
301
|
const out = join(root, "src/generated");
|
|
239
302
|
await mkdir(out, { recursive: true });
|
|
240
303
|
const written = [];
|
|
241
304
|
const dirs = ["src/core/*", "src/modules/*"];
|
|
242
305
|
const routes = await find(root, dirs.map((d) => `${d}/routes/*.routes.ts`));
|
|
243
|
-
await
|
|
306
|
+
await write(join(out, "routes.ts"), listModule(routes, {
|
|
244
307
|
prefix: "routes",
|
|
245
308
|
typeImport: "import type { RouteDefinition } from \"#core/http/routes\";",
|
|
246
309
|
typeName: "RouteDefinition",
|
|
247
310
|
exportName: "routes"
|
|
248
311
|
}), written);
|
|
249
312
|
const commands = await find(root, dirs.map((d) => `${d}/commands/*.ts`));
|
|
250
|
-
await
|
|
313
|
+
await write(join(out, "commands.ts"), listWithPackages(commands, await contributions(project, "commands"), {
|
|
251
314
|
prefix: "command",
|
|
252
315
|
typeImport: "import type { CommandDefinition } from \"@tatara/core\";",
|
|
253
316
|
typeName: "CommandDefinition",
|
|
@@ -255,14 +318,14 @@ async function sync(project = loadProject()) {
|
|
|
255
318
|
}), written);
|
|
256
319
|
if (project.has("jobs")) {
|
|
257
320
|
const consumers = await find(root, dirs.map((d) => `${d}/consumers/*.ts`));
|
|
258
|
-
await
|
|
321
|
+
await write(join(out, "consumers.ts"), listModule(consumers, {
|
|
259
322
|
prefix: "consumer",
|
|
260
323
|
typeImport: "import type { ConsumerDefinition } from \"@tatara/jobs\";",
|
|
261
324
|
typeName: "ConsumerDefinition",
|
|
262
325
|
exportName: "consumers"
|
|
263
326
|
}), written);
|
|
264
327
|
const schedules = await find(root, dirs.map((d) => `${d}/schedules/*.ts`));
|
|
265
|
-
await
|
|
328
|
+
await write(join(out, "schedules.ts"), listModule(schedules, {
|
|
266
329
|
prefix: "schedule",
|
|
267
330
|
typeImport: "import type { ScheduleDefinition } from \"@tatara/jobs\";",
|
|
268
331
|
typeName: "ScheduleDefinition",
|
|
@@ -273,7 +336,7 @@ async function sync(project = loadProject()) {
|
|
|
273
336
|
const permissions = await find(root, ["src/modules/*/permissions/*.permissions.ts"]);
|
|
274
337
|
const imports = permissions.map((f) => `import ${identifier(f, "permissions")} from "${specifier(f)}";`);
|
|
275
338
|
const names = permissions.map((f) => identifier(f, "permissions"));
|
|
276
|
-
await
|
|
339
|
+
await write(join(out, "permissions.ts"), `${HEADER}import { authzPermissions, mergeCatalogs, type PermissionKeysOf } from "@tatara/authz";
|
|
277
340
|
${imports.join("\n")}${imports.length ? "\n" : ""}
|
|
278
341
|
export const catalog = mergeCatalogs(authzPermissions${names.map((n) => `, ${n}`).join("")});
|
|
279
342
|
|
|
@@ -283,7 +346,7 @@ export type PermissionKey =
|
|
|
283
346
|
}
|
|
284
347
|
if (project.config.database !== "none") {
|
|
285
348
|
const seeds = await find(root, dirs.map((d) => `${d}/db/seed.ts`));
|
|
286
|
-
await
|
|
349
|
+
await write(join(out, "seeds.ts"), listWithPackages(seeds, await contributions(project, "seeds"), {
|
|
287
350
|
prefix: "seed",
|
|
288
351
|
typeImport: "import type { SeedDefinition } from \"@tatara/db\";",
|
|
289
352
|
typeName: "SeedDefinition",
|
|
@@ -292,7 +355,7 @@ export type PermissionKey =
|
|
|
292
355
|
}
|
|
293
356
|
if (project.config.orm === "drizzle") {
|
|
294
357
|
const schemas = await find(root, dirs.map((d) => `${d}/db/*.schema.ts`));
|
|
295
|
-
await
|
|
358
|
+
await write(join(out, "schema.ts"), `${HEADER}${schemas.map((f) => `export * from "${specifier(f)}";`).join("\n")}\nexport {};\n`, written);
|
|
296
359
|
}
|
|
297
360
|
if (project.config.orm === "prisma") {
|
|
298
361
|
const bin = join(root, "node_modules/.bin/prisma");
|
|
@@ -650,6 +713,125 @@ function printDoctor(checks) {
|
|
|
650
713
|
}
|
|
651
714
|
}
|
|
652
715
|
//#endregion
|
|
716
|
+
//#region src/commands/eject.ts
|
|
717
|
+
var EjectError = class extends Error {};
|
|
718
|
+
const short = (name) => name.replace(/^@tatara\//, "");
|
|
719
|
+
const full = (name) => `@tatara/${short(name)}`;
|
|
720
|
+
const NOT_LIBRARIES = /* @__PURE__ */ new Set(["cli", "create-tatara"]);
|
|
721
|
+
async function readJson(path) {
|
|
722
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Copies a package's source into src/ejected/<name>, points the app's imports at it and records
|
|
726
|
+
* it in tatara.json. From then on the app owns that code and `tatara upgrade` leaves it alone.
|
|
727
|
+
*/
|
|
728
|
+
async function eject(project, name, opts = {}) {
|
|
729
|
+
const target = short(name);
|
|
730
|
+
if (NOT_LIBRARIES.has(target)) throw new EjectError(`${full(target)} is a tool, not a library`);
|
|
731
|
+
const appPkgPath = join(project.root, "package.json");
|
|
732
|
+
const appPkg = await readJson(appPkgPath);
|
|
733
|
+
const installed = Object.keys({
|
|
734
|
+
...appPkg.dependencies,
|
|
735
|
+
...appPkg.devDependencies
|
|
736
|
+
}).filter((dep) => dep.startsWith("@tatara/") && !NOT_LIBRARIES.has(short(dep))).map(short);
|
|
737
|
+
const already = ejectedPackages(project.config);
|
|
738
|
+
if (already.includes(target)) throw new EjectError(`${full(target)} is already ejected`);
|
|
739
|
+
if (!installed.includes(target)) throw new EjectError(`${full(target)} is not a dependency of this app`);
|
|
740
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
741
|
+
for (const dep of installed) {
|
|
742
|
+
const path = join(project.root, "node_modules", full(dep), "package.json");
|
|
743
|
+
if (existsSync(path)) manifests.set(dep, await readJson(path));
|
|
744
|
+
}
|
|
745
|
+
const chain = [target];
|
|
746
|
+
for (let i = 0; i < chain.length; i++) for (const [dep, manifest] of manifests) {
|
|
747
|
+
if (chain.includes(dep)) continue;
|
|
748
|
+
if (full(chain[i]) in (manifest.peerDependencies ?? {})) chain.push(dep);
|
|
749
|
+
}
|
|
750
|
+
const dependents = chain.slice(1);
|
|
751
|
+
if (dependents.length > 0 && !opts.withDependents) throw new EjectError(`${dependents.map(full).join(", ")} depend on ${full(target)}. Eject them too with --with-dependents.`);
|
|
752
|
+
const report = {
|
|
753
|
+
ejected: chain,
|
|
754
|
+
copied: [],
|
|
755
|
+
rewritten: [],
|
|
756
|
+
added: []
|
|
757
|
+
};
|
|
758
|
+
const versions = {};
|
|
759
|
+
for (const dep of chain) {
|
|
760
|
+
const manifest = manifests.get(dep);
|
|
761
|
+
const source = join(project.root, "node_modules", full(dep), "src");
|
|
762
|
+
if (!manifest || !existsSync(source)) throw new EjectError(`${full(dep)} ships no source to eject; update it to a version that does, then run this again`);
|
|
763
|
+
versions[dep] = manifest.version;
|
|
764
|
+
const files = await glob(["**/*.ts"], {
|
|
765
|
+
cwd: source,
|
|
766
|
+
ignore: ["**/*.test.ts"]
|
|
767
|
+
});
|
|
768
|
+
for (const file of files) {
|
|
769
|
+
const to = `src/ejected/${dep}/${file}`;
|
|
770
|
+
await mkdir(dirname(join(project.root, to)), { recursive: true });
|
|
771
|
+
await cp(join(source, file), join(project.root, to));
|
|
772
|
+
report.copied.push(to);
|
|
773
|
+
}
|
|
774
|
+
if (manifest.tatara) {
|
|
775
|
+
const to = `src/ejected/${dep}/tatara-package.json`;
|
|
776
|
+
await writeFile(join(project.root, to), `${JSON.stringify(manifest.tatara, null, " ")}\n`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const ejected = [...already, ...chain];
|
|
780
|
+
for (const file of await glob(["src/**/*.ts", "tests/**/*.ts"], { cwd: project.root })) {
|
|
781
|
+
const path = join(project.root, file);
|
|
782
|
+
const text = await readFile(path, "utf8");
|
|
783
|
+
const next = pointAtEjected(text, ejected);
|
|
784
|
+
if (next !== text) {
|
|
785
|
+
await writeFile(path, next);
|
|
786
|
+
if (!report.copied.includes(file)) report.rewritten.push(file);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const deps = { ...appPkg.dependencies };
|
|
790
|
+
const listed = new Set(Object.keys({
|
|
791
|
+
...appPkg.dependencies,
|
|
792
|
+
...appPkg.devDependencies
|
|
793
|
+
}));
|
|
794
|
+
for (const dep of chain) {
|
|
795
|
+
const manifest = manifests.get(dep);
|
|
796
|
+
for (const [peer, range] of Object.entries(manifest.peerDependencies ?? {})) {
|
|
797
|
+
if (listed.has(peer) || manifest.peerDependenciesMeta?.[peer]?.optional) continue;
|
|
798
|
+
if (peer.startsWith("@tatara/") && ejected.includes(short(peer))) continue;
|
|
799
|
+
deps[peer] = range;
|
|
800
|
+
listed.add(peer);
|
|
801
|
+
report.added.push(peer);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
for (const dep of chain) delete deps[full(dep)];
|
|
805
|
+
const devDeps = { ...appPkg.devDependencies };
|
|
806
|
+
for (const dep of chain) delete devDeps[full(dep)];
|
|
807
|
+
const nextPkg = {
|
|
808
|
+
...appPkg,
|
|
809
|
+
imports: {
|
|
810
|
+
...appPkg.imports,
|
|
811
|
+
"#ejected/*": "./src/ejected/*.ts"
|
|
812
|
+
},
|
|
813
|
+
dependencies: Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b))),
|
|
814
|
+
devDependencies: devDeps
|
|
815
|
+
};
|
|
816
|
+
await writeFile(appPkgPath, `${JSON.stringify(nextPkg, null, " ")}\n`);
|
|
817
|
+
const configPath = join(project.root, "tatara.json");
|
|
818
|
+
const config = await readJson(configPath);
|
|
819
|
+
await writeFile(configPath, `${JSON.stringify({
|
|
820
|
+
...config,
|
|
821
|
+
ejected: {
|
|
822
|
+
...config.ejected,
|
|
823
|
+
...versions
|
|
824
|
+
}
|
|
825
|
+
}, null, " ")}\n`);
|
|
826
|
+
if (opts.formatFile) for (const file of [
|
|
827
|
+
...report.copied,
|
|
828
|
+
...report.rewritten,
|
|
829
|
+
"package.json",
|
|
830
|
+
"tatara.json"
|
|
831
|
+
]) await opts.formatFile(file, project);
|
|
832
|
+
return report;
|
|
833
|
+
}
|
|
834
|
+
//#endregion
|
|
653
835
|
//#region src/paths.ts
|
|
654
836
|
/** Package root: one level above both src/ (dev) and dist/ (published, bundled). */
|
|
655
837
|
const PACKAGE_ROOT = resolve(import.meta.dirname, "..");
|
|
@@ -2254,6 +2436,10 @@ function pnpmWorkspace(features) {
|
|
|
2254
2436
|
lines.push("# tatara's own packages are installed as soon as they are released", "minimumReleaseAgeExclude:", " - \"@tatara/*\"");
|
|
2255
2437
|
return `${lines.join("\n")}\n`;
|
|
2256
2438
|
}
|
|
2439
|
+
/** Tests of tatara's wiring, not of the app; generated apps leave them out. */
|
|
2440
|
+
function isFrameworkTest(path) {
|
|
2441
|
+
return /^src\/core\/.+\/tests\/[^/]+\.test\.ts$/.test(path);
|
|
2442
|
+
}
|
|
2257
2443
|
/** Folds applicable variants into each feature so later steps see one flat definition. */
|
|
2258
2444
|
function effectiveFeatures(options) {
|
|
2259
2445
|
const features = resolveFeatures(options);
|
|
@@ -2277,6 +2463,9 @@ async function renderApp(options, generate = {}) {
|
|
|
2277
2463
|
const selected = new Set(features.map((f) => f.id));
|
|
2278
2464
|
const tokenValues = tokens(options);
|
|
2279
2465
|
const files = await collectTemplateFiles(features, selected);
|
|
2466
|
+
if (!generate.frameworkTests) {
|
|
2467
|
+
for (const path of files.keys()) if (isFrameworkTest(path)) files.delete(path);
|
|
2468
|
+
}
|
|
2280
2469
|
const env = dedupeEnv(features, tokenValues);
|
|
2281
2470
|
files.set("src/core/config/env.ts", envModule(env.filter((v) => !v.composeOnly)));
|
|
2282
2471
|
files.set(".env.example", envFile(env, "example"));
|
|
@@ -2580,7 +2769,7 @@ async function generate(project, request) {
|
|
|
2580
2769
|
for (const [rel, text] of rendered) {
|
|
2581
2770
|
const full = join(project.root, rel);
|
|
2582
2771
|
await mkdir(dirname(full), { recursive: true });
|
|
2583
|
-
await writeFile(full, text);
|
|
2772
|
+
await writeFile(full, pointAtEjected(text, ejectedPackages(project.config)));
|
|
2584
2773
|
}
|
|
2585
2774
|
if (!request.skipPostProcess) await postProcess(project, files);
|
|
2586
2775
|
return {
|
|
@@ -2869,7 +3058,10 @@ async function newApp(dir, rawFlags) {
|
|
|
2869
3058
|
const name = root.split(/[\\/]/).at(-1) ?? "app";
|
|
2870
3059
|
const options = validateOptions(interactive ? await prompt(name, flags) : fromFlags(name, flags));
|
|
2871
3060
|
const pm = pmCommands(options.packageManager);
|
|
2872
|
-
await writeFiles(root, await renderApp(options, {
|
|
3061
|
+
await writeFiles(root, await renderApp(options, {
|
|
3062
|
+
pmVersion: await pmVersion(options.packageManager),
|
|
3063
|
+
frameworkTests: flags.frameworkTests
|
|
3064
|
+
}), { env: devEnvFromExample(options) });
|
|
2873
3065
|
const project = loadProject(root);
|
|
2874
3066
|
await afterCreate(project, "files");
|
|
2875
3067
|
if (flags.install !== false) {
|
|
@@ -3255,10 +3447,7 @@ async function blockersOf$1(ctx, group, sources) {
|
|
|
3255
3447
|
return blockers;
|
|
3256
3448
|
}
|
|
3257
3449
|
async function applyGroup(ctx, group) {
|
|
3258
|
-
for (const path of group.regenerate)
|
|
3259
|
-
const next = await ctx.renderCurrent(path);
|
|
3260
|
-
if (next !== void 0) await ctx.write(path, next);
|
|
3261
|
-
}
|
|
3450
|
+
for (const path of group.regenerate) await ctx.regenerate(path);
|
|
3262
3451
|
for (const path of group.remove) await ctx.removeIfUnchanged(path);
|
|
3263
3452
|
for (const rewrite of group.rewrites) await ctx.rewriteImports(rewrite.from, rewrite.to);
|
|
3264
3453
|
}
|
|
@@ -3504,8 +3693,7 @@ const authPackage = {
|
|
|
3504
3693
|
for (const group of ready) {
|
|
3505
3694
|
for (const path of group.regenerate) {
|
|
3506
3695
|
if (await read$3(ctx, path) === void 0) continue;
|
|
3507
|
-
|
|
3508
|
-
if (next !== void 0) await ctx.write(path, next);
|
|
3696
|
+
await ctx.regenerate(path);
|
|
3509
3697
|
}
|
|
3510
3698
|
for (const path of group.remove) await ctx.removeIfUnchanged(path);
|
|
3511
3699
|
}
|
|
@@ -3650,15 +3838,43 @@ const authzPackage = {
|
|
|
3650
3838
|
ctx.report.blocked.push("authz-package");
|
|
3651
3839
|
return;
|
|
3652
3840
|
}
|
|
3653
|
-
for (const path of regenerate)
|
|
3654
|
-
const next = await ctx.renderCurrent(path);
|
|
3655
|
-
if (next !== void 0) await ctx.write(path, next);
|
|
3656
|
-
}
|
|
3841
|
+
for (const path of regenerate) await ctx.regenerate(path);
|
|
3657
3842
|
for (const path of removed) await ctx.removeIfUnchanged(path);
|
|
3658
3843
|
for (const move of MOVES) await ctx.rewriteImports(move.from, move.to);
|
|
3659
3844
|
}
|
|
3660
3845
|
};
|
|
3661
3846
|
//#endregion
|
|
3847
|
+
//#region src/upgrade/codemods/authz-stores.ts
|
|
3848
|
+
const group = {
|
|
3849
|
+
name: "authz store",
|
|
3850
|
+
regenerate: ["src/core/authz/index.ts"],
|
|
3851
|
+
remove: ["src/core/authz/db/store.ts"],
|
|
3852
|
+
rewrites: [{
|
|
3853
|
+
from: "#core/authz/db/store",
|
|
3854
|
+
to: "#core/authz/index",
|
|
3855
|
+
names: []
|
|
3856
|
+
}]
|
|
3857
|
+
};
|
|
3858
|
+
const authzStores = {
|
|
3859
|
+
version: "0.9.0",
|
|
3860
|
+
id: "authz-stores",
|
|
3861
|
+
description: "Use the Prisma or Drizzle authz store from @tatara/authz",
|
|
3862
|
+
async run(ctx) {
|
|
3863
|
+
if (!ctx.project.config.addons.includes("authz")) return;
|
|
3864
|
+
if (ejectedPackages(ctx.project.config).includes("authz")) {
|
|
3865
|
+
ctx.report.notes.push("@tatara/authz is ejected; its store stays in src/core/authz/db/store.ts");
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
const blockers = await blockersOf$1(ctx, group, await sourceFiles(ctx));
|
|
3869
|
+
if (blockers.length > 0) {
|
|
3870
|
+
ctx.report.notes.push(`The authz store was not moved onto @tatara/authz: ${blockers.join("; ")}. Pass prismaAuthzStore(db) or drizzleAuthzStore(db, tables) to createAuthz by hand.`);
|
|
3871
|
+
ctx.report.blocked.push("authz-stores");
|
|
3872
|
+
return;
|
|
3873
|
+
}
|
|
3874
|
+
await applyGroup(ctx, group);
|
|
3875
|
+
}
|
|
3876
|
+
};
|
|
3877
|
+
//#endregion
|
|
3662
3878
|
//#region src/upgrade/codemods/core-package.ts
|
|
3663
3879
|
const CORE = "@tatara/core";
|
|
3664
3880
|
/** Files @tatara/core replaces, with the import specifier apps used for each. */
|
|
@@ -3823,6 +4039,20 @@ const dbTesting = {
|
|
|
3823
4039
|
}
|
|
3824
4040
|
};
|
|
3825
4041
|
//#endregion
|
|
4042
|
+
//#region src/upgrade/codemods/framework-tests.ts
|
|
4043
|
+
const frameworkTests = {
|
|
4044
|
+
version: "0.9.0",
|
|
4045
|
+
id: "framework-tests",
|
|
4046
|
+
description: "Remove tatara's own wiring tests from src/core; the packages and tatara's e2e cover them",
|
|
4047
|
+
async run(ctx) {
|
|
4048
|
+
for (const path of await sourceFiles(ctx)) {
|
|
4049
|
+
if (!isFrameworkTest(path)) continue;
|
|
4050
|
+
if (!await ctx.baseline.isUnchanged(path, await ctx.read(path))) continue;
|
|
4051
|
+
await ctx.removeIfUnchanged(path);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
};
|
|
4055
|
+
//#endregion
|
|
3826
4056
|
//#region src/upgrade/codemods/http-package.ts
|
|
3827
4057
|
const HTTP = "@tatara/http";
|
|
3828
4058
|
/** Wiring files the app keeps; replaced by what this CLI generates when the app never changed them. */
|
|
@@ -3885,58 +4115,94 @@ async function outsideImporters(ctx) {
|
|
|
3885
4115
|
}
|
|
3886
4116
|
return out;
|
|
3887
4117
|
}
|
|
4118
|
+
const httpPackage = {
|
|
4119
|
+
version: "0.3.0",
|
|
4120
|
+
id: "http-package",
|
|
4121
|
+
description: "Use @tatara/http for app setup, errors, the envelope, OpenAPI docs and the server",
|
|
4122
|
+
async run(ctx) {
|
|
4123
|
+
await ctx.addDependency(HTTP, versionOf(HTTP));
|
|
4124
|
+
const blockers = [];
|
|
4125
|
+
const replace = [];
|
|
4126
|
+
for (const path of [
|
|
4127
|
+
APP,
|
|
4128
|
+
ROUTES,
|
|
4129
|
+
...SERVERS
|
|
4130
|
+
]) {
|
|
4131
|
+
const text = await read(ctx, path);
|
|
4132
|
+
if (text === void 0) continue;
|
|
4133
|
+
if (await ctx.baseline.isUnchanged(path, text)) replace.push(path);
|
|
4134
|
+
else blockers.push(`${path} is customised`);
|
|
4135
|
+
}
|
|
4136
|
+
const present = [];
|
|
4137
|
+
for (const entry of MOVED) {
|
|
4138
|
+
const text = await read(ctx, entry.path);
|
|
4139
|
+
if (text === void 0) continue;
|
|
4140
|
+
present.push(entry);
|
|
4141
|
+
if (!await ctx.baseline.isUnchanged(entry.path, text)) blockers.push(`${entry.path} is customised`);
|
|
4142
|
+
}
|
|
4143
|
+
for (const path of await outsideImporters(ctx)) blockers.push(`${path} imports one of the moved files`);
|
|
4144
|
+
if (blockers.length > 0) {
|
|
4145
|
+
ctx.report.notes.push(`Nothing in src/core/http was changed: ${blockers.join("; ")}. Move these onto @tatara/http (createApp, mountRoutes, serveOpenApi, handleErrors, startServer and its error classes) by hand, then delete the old files.`);
|
|
4146
|
+
ctx.report.blocked.push("http-package");
|
|
4147
|
+
return;
|
|
4148
|
+
}
|
|
4149
|
+
for (const path of replace) await ctx.regenerate(path);
|
|
4150
|
+
for (const entry of present) await ctx.removeIfUnchanged(entry.path);
|
|
4151
|
+
for (const specifier of [
|
|
4152
|
+
"#core/http/errors",
|
|
4153
|
+
"#core/http/response",
|
|
4154
|
+
"#core/http/middleware/validation"
|
|
4155
|
+
]) await ctx.rewriteImports(specifier, HTTP);
|
|
4156
|
+
}
|
|
4157
|
+
};
|
|
4158
|
+
//#endregion
|
|
4159
|
+
//#region src/upgrade/codemods/package-commands.ts
|
|
4160
|
+
/** Copies whose commands and seeds the packages contribute now (wired by `tatara sync`). */
|
|
4161
|
+
const COPIES = [
|
|
4162
|
+
"src/core/db/commands/migrate.ts",
|
|
4163
|
+
"src/core/db/commands/reset.ts",
|
|
4164
|
+
"src/core/db/commands/seed.ts",
|
|
4165
|
+
"src/core/authz/commands/grant-role.ts",
|
|
4166
|
+
"src/core/authz/commands/sync-permissions.ts",
|
|
4167
|
+
"src/core/authz/db/seed.ts",
|
|
4168
|
+
"src/core/email/commands/send-test.ts",
|
|
4169
|
+
"src/core/storage/commands/check.ts",
|
|
4170
|
+
"src/core/jobs/commands/enqueue.ts"
|
|
4171
|
+
];
|
|
4172
|
+
const specifierOf = (path) => `#${path.replace(/^src\//, "").replace(/\.ts$/, "")}`;
|
|
3888
4173
|
//#endregion
|
|
3889
4174
|
//#region src/upgrade/codemods.ts
|
|
3890
4175
|
/** Every codemod tatara ships, oldest first. Each step that moves code into a package adds one. */
|
|
3891
4176
|
const CODEMODS = [
|
|
3892
4177
|
corePackage,
|
|
4178
|
+
httpPackage,
|
|
4179
|
+
dbTesting,
|
|
4180
|
+
authPackage,
|
|
4181
|
+
authzPackage,
|
|
4182
|
+
addonPackages,
|
|
4183
|
+
frameworkTests,
|
|
4184
|
+
authzStores,
|
|
3893
4185
|
{
|
|
3894
|
-
version: "0.
|
|
3895
|
-
id: "
|
|
3896
|
-
description: "
|
|
4186
|
+
version: "0.9.0",
|
|
4187
|
+
id: "package-commands",
|
|
4188
|
+
description: "Take db, authz, email, storage and jobs commands and seeds from their packages",
|
|
3897
4189
|
async run(ctx) {
|
|
3898
|
-
await ctx
|
|
3899
|
-
const
|
|
3900
|
-
const
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
const present = [];
|
|
3912
|
-
for (const entry of MOVED) {
|
|
3913
|
-
const text = await read(ctx, entry.path);
|
|
3914
|
-
if (text === void 0) continue;
|
|
3915
|
-
present.push(entry);
|
|
3916
|
-
if (!await ctx.baseline.isUnchanged(entry.path, text)) blockers.push(`${entry.path} is customised`);
|
|
3917
|
-
}
|
|
3918
|
-
for (const path of await outsideImporters(ctx)) blockers.push(`${path} imports one of the moved files`);
|
|
3919
|
-
if (blockers.length > 0) {
|
|
3920
|
-
ctx.report.notes.push(`Nothing in src/core/http was changed: ${blockers.join("; ")}. Move these onto @tatara/http (createApp, mountRoutes, serveOpenApi, handleErrors, startServer and its error classes) by hand, then delete the old files.`);
|
|
3921
|
-
ctx.report.blocked.push("http-package");
|
|
3922
|
-
return;
|
|
3923
|
-
}
|
|
3924
|
-
for (const path of replace) {
|
|
3925
|
-
const next = await ctx.renderCurrent(path);
|
|
3926
|
-
if (next !== void 0) await ctx.write(path, next);
|
|
4190
|
+
const sources = await sourceFiles(ctx);
|
|
4191
|
+
const texts = /* @__PURE__ */ new Map();
|
|
4192
|
+
for (const path of sources) texts.set(path, await ctx.read(path));
|
|
4193
|
+
const ejected = ejectedPackages(ctx.project.config);
|
|
4194
|
+
for (const path of COPIES) {
|
|
4195
|
+
if (!texts.has(path)) continue;
|
|
4196
|
+
if (ejected.includes(path.split("/")[2] ?? "")) continue;
|
|
4197
|
+
const importers = [...texts].filter(([other, text]) => other !== path && importsOf(text, specifierOf(path)));
|
|
4198
|
+
if (importers.length > 0) {
|
|
4199
|
+
ctx.report.notes.push(`${path} was kept: ${importers.map(([p]) => p).join(", ")} import it.`);
|
|
4200
|
+
continue;
|
|
4201
|
+
}
|
|
4202
|
+
await ctx.removeIfUnchanged(path);
|
|
3927
4203
|
}
|
|
3928
|
-
for (const entry of present) await ctx.removeIfUnchanged(entry.path);
|
|
3929
|
-
for (const specifier of [
|
|
3930
|
-
"#core/http/errors",
|
|
3931
|
-
"#core/http/response",
|
|
3932
|
-
"#core/http/middleware/validation"
|
|
3933
|
-
]) await ctx.rewriteImports(specifier, HTTP);
|
|
3934
4204
|
}
|
|
3935
|
-
}
|
|
3936
|
-
dbTesting,
|
|
3937
|
-
authPackage,
|
|
3938
|
-
authzPackage,
|
|
3939
|
-
addonPackages
|
|
4205
|
+
}
|
|
3940
4206
|
];
|
|
3941
4207
|
//#endregion
|
|
3942
4208
|
//#region src/upgrade/context.ts
|
|
@@ -3953,7 +4219,9 @@ function createContext(project, baseline, opts = {}) {
|
|
|
3953
4219
|
};
|
|
3954
4220
|
const abs = (path) => join(project.root, path);
|
|
3955
4221
|
let rendered;
|
|
3956
|
-
const
|
|
4222
|
+
const ejected = ejectedPackages(project.config);
|
|
4223
|
+
const write = async (path, written) => {
|
|
4224
|
+
const text = path.endsWith(".ts") ? pointAtEjected(written, ejected) : written;
|
|
3957
4225
|
if (!report.written.includes(path)) {
|
|
3958
4226
|
report.written.push(path);
|
|
3959
4227
|
const before = existsSync(abs(path)) ? await readFile(abs(path), "utf8") : void 0;
|
|
@@ -3963,7 +4231,7 @@ function createContext(project, baseline, opts = {}) {
|
|
|
3963
4231
|
await mkdir(dirname(abs(path)), { recursive: true });
|
|
3964
4232
|
await writeFile(abs(path), text);
|
|
3965
4233
|
};
|
|
3966
|
-
|
|
4234
|
+
const ctx = {
|
|
3967
4235
|
project,
|
|
3968
4236
|
baseline,
|
|
3969
4237
|
report,
|
|
@@ -4013,7 +4281,30 @@ function createContext(project, baseline, opts = {}) {
|
|
|
4013
4281
|
});
|
|
4014
4282
|
return (await rendered).get(path);
|
|
4015
4283
|
},
|
|
4284
|
+
async regenerate(path) {
|
|
4285
|
+
const next = await ctx.renderCurrent(path);
|
|
4286
|
+
if (next === void 0) {
|
|
4287
|
+
const spec = `#${path.replace(/^src\//, "").replace(/\.ts$/, "")}`;
|
|
4288
|
+
const importers = [];
|
|
4289
|
+
for (const other of await glob(["src/**/*.ts", "tests/**/*.ts"], {
|
|
4290
|
+
cwd: project.root,
|
|
4291
|
+
ignore: ["src/generated/**"]
|
|
4292
|
+
})) if (other !== path && importsOf(await readFile(abs(other), "utf8"), spec)) importers.push(other);
|
|
4293
|
+
if (importers.length > 0) {
|
|
4294
|
+
report.notes.push(`${path} was kept: ${importers.join(", ")} import it.`);
|
|
4295
|
+
return "kept";
|
|
4296
|
+
}
|
|
4297
|
+
return ctx.removeIfUnchanged(path);
|
|
4298
|
+
}
|
|
4299
|
+
await write(path, next);
|
|
4300
|
+
return "written";
|
|
4301
|
+
},
|
|
4016
4302
|
async addDependency(name, range, { dev = false } = {}) {
|
|
4303
|
+
if (ejected.includes(name.replace(/^@tatara\//, ""))) {
|
|
4304
|
+
const note = `${name} is ejected; it was not added (its code is in src/ejected)`;
|
|
4305
|
+
if (!report.notes.includes(note)) report.notes.push(note);
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4017
4308
|
const pkg = JSON.parse(await readFile(abs("package.json"), "utf8"));
|
|
4018
4309
|
const key = dev ? "devDependencies" : "dependencies";
|
|
4019
4310
|
pkg[key] = {
|
|
@@ -4023,9 +4314,33 @@ function createContext(project, baseline, opts = {}) {
|
|
|
4023
4314
|
await write("package.json", `${JSON.stringify(pkg, null, " ")}\n`);
|
|
4024
4315
|
}
|
|
4025
4316
|
};
|
|
4317
|
+
return ctx;
|
|
4026
4318
|
}
|
|
4027
4319
|
//#endregion
|
|
4028
4320
|
//#region src/commands/upgrade.ts
|
|
4321
|
+
async function pmInstall(project) {
|
|
4322
|
+
const [cmd = "pnpm", ...args] = pmCommands(project.config.packageManager).install;
|
|
4323
|
+
if ((await x(cmd, args, {
|
|
4324
|
+
throwOnError: false,
|
|
4325
|
+
nodeOptions: {
|
|
4326
|
+
cwd: project.root,
|
|
4327
|
+
stdio: "inherit"
|
|
4328
|
+
}
|
|
4329
|
+
})).exitCode !== 0) throw new Error(`${cmd} ${args.join(" ")} failed; fix it, then run tatara upgrade again`);
|
|
4330
|
+
}
|
|
4331
|
+
/** Sets every @tatara/* library the app depends on to the range this CLI ships with. */
|
|
4332
|
+
async function bumpPackages(ctx) {
|
|
4333
|
+
const pkg = JSON.parse(await ctx.read("package.json"));
|
|
4334
|
+
const known = templateVersions();
|
|
4335
|
+
for (const dev of [false, true]) for (const [name, range] of Object.entries((dev ? pkg.devDependencies : pkg.dependencies) ?? {})) {
|
|
4336
|
+
if (!name.startsWith("@tatara/") || name === "@tatara/cli" || !(name in known)) continue;
|
|
4337
|
+
const current = /^\^(\d+\.\d+\.\d+)$/.exec(range)?.[1];
|
|
4338
|
+
const next = versionOf(name);
|
|
4339
|
+
const shipped = /^\^(\d+\.\d+\.\d+)$/.exec(next)?.[1];
|
|
4340
|
+
if (!current || !shipped || compareVersions(current, shipped) >= 0) continue;
|
|
4341
|
+
await ctx.addDependency(name, next, { dev });
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4029
4344
|
async function biomeFormat(file, project) {
|
|
4030
4345
|
const biome = join(project.root, "node_modules", ".bin", "biome");
|
|
4031
4346
|
if (!existsSync(biome)) return;
|
|
@@ -4072,9 +4387,11 @@ async function upgrade(project, opts = {}) {
|
|
|
4072
4387
|
ctx.report.ran.push(mod.id);
|
|
4073
4388
|
if (ctx.report.blocked.length > blockedBefore && reached === target) reached = due.slice(0, due.indexOf(mod)).at(-1)?.version ?? from;
|
|
4074
4389
|
}
|
|
4390
|
+
await bumpPackages(ctx);
|
|
4075
4391
|
if (opts.dryRun) return ctx.report;
|
|
4076
4392
|
const format = opts.formatFile ?? biomeFormat;
|
|
4077
4393
|
for (const path of ctx.report.written) await format(path, project);
|
|
4394
|
+
await (opts.runInstall ?? pmInstall)(project);
|
|
4078
4395
|
await (opts.runSync ?? sync)(project);
|
|
4079
4396
|
const path = join(project.root, "tatara.json");
|
|
4080
4397
|
const config = JSON.parse(await readFile(path, "utf8"));
|
|
@@ -4107,7 +4424,7 @@ function printUpgrade(report, from, to) {
|
|
|
4107
4424
|
//#endregion
|
|
4108
4425
|
//#region src/bin.ts
|
|
4109
4426
|
const program = new Command("tatara").description("Create and grow opinionated Hono backends").version(cliVersion());
|
|
4110
|
-
program.command("new").description("Create a new app").argument("[dir]", "directory to create").option("--orm <orm>", "drizzle | prisma | none").option("--db <database>", "postgres | mysql | none").option("--auth <provider>", "better-auth | none (keycloak = better-auth with --auth-methods keycloak)").option("--auth-methods <list>", "email-otp,phone-otp,password,orgs (better-auth)").option("--addons <list>", "authz,jobs,admin,storage,email,i18n").option("--pm <pm>", "pnpm | npm | bun").option("-y, --yes", "skip prompts and use flags/defaults").option("--no-install", "do not install dependencies").option("--no-git", "do not initialise git").action(async (dir, flags) => {
|
|
4427
|
+
program.command("new").description("Create a new app").argument("[dir]", "directory to create").option("--orm <orm>", "drizzle | prisma | none").option("--db <database>", "postgres | mysql | none").option("--auth <provider>", "better-auth | none (keycloak = better-auth with --auth-methods keycloak)").option("--auth-methods <list>", "email-otp,phone-otp,password,orgs (better-auth)").option("--addons <list>", "authz,jobs,admin,storage,email,i18n").option("--pm <pm>", "pnpm | npm | bun").option("-y, --yes", "skip prompts and use flags/defaults").option("--no-install", "do not install dependencies").option("--no-git", "do not initialise git").addOption(new Option("--framework-tests", "include tatara's own wiring tests (e2e)").hideHelp()).action(async (dir, flags) => {
|
|
4111
4428
|
await newApp(dir, flags);
|
|
4112
4429
|
});
|
|
4113
4430
|
program.command("sync").description("Regenerate src/generated from discovered module files").option("--quiet", "no output").action(async (flags) => {
|
|
@@ -4182,6 +4499,17 @@ program.command("upgrade").description("Run this CLI version's codemods on the a
|
|
|
4182
4499
|
printUpgrade(await upgrade(project, { dryRun: flags.dryRun }), from, cliVersion());
|
|
4183
4500
|
if (!flags.dryRun && flags.verify !== false && !await verify(loadProject())) process.exitCode = 1;
|
|
4184
4501
|
});
|
|
4502
|
+
program.command("eject <package>").description("Copy a @tatara/* package's source into src/ejected; the app owns it from then on").option("--with-dependents", "also eject installed @tatara/* packages that depend on it").action(async (name, flags) => {
|
|
4503
|
+
const report = await eject(loadProject(), name, {
|
|
4504
|
+
withDependents: flags.withDependents,
|
|
4505
|
+
formatFile: biomeFormat
|
|
4506
|
+
});
|
|
4507
|
+
const names = report.ejected.map((n) => `@tatara/${n}`).join(", ");
|
|
4508
|
+
console.log(pc.green(`ejected ${names} into src/ejected (${report.copied.length} files)`));
|
|
4509
|
+
if (report.rewritten.length > 0) console.log(` ${report.rewritten.length} files now import the ejected copy`);
|
|
4510
|
+
if (report.added.length > 0) console.log(` added dependencies: ${report.added.join(", ")}`);
|
|
4511
|
+
console.log(` run your package manager's install, then verify; tatara upgrade skips ${names} from now on`);
|
|
4512
|
+
});
|
|
4185
4513
|
program.command("doctor").description("Check the toolchain, services, env and layout").option("--json", "machine-readable output").action(async (flags) => {
|
|
4186
4514
|
const checks = await doctor(loadProject());
|
|
4187
4515
|
if (flags.json) console.log(JSON.stringify(checks, null, 2));
|
|
@@ -4189,7 +4517,7 @@ program.command("doctor").description("Check the toolchain, services, env and la
|
|
|
4189
4517
|
if (checks.some((c) => !c.ok)) process.exitCode = 1;
|
|
4190
4518
|
});
|
|
4191
4519
|
program.parseAsync().catch((error) => {
|
|
4192
|
-
if (error instanceof OptionsError || error instanceof NotATataraProject || error instanceof GeneratorError || error instanceof DbError) console.error(pc.red(error.message));
|
|
4520
|
+
if (error instanceof OptionsError || error instanceof EjectError || error instanceof NotATataraProject || error instanceof GeneratorError || error instanceof DbError) console.error(pc.red(error.message));
|
|
4193
4521
|
else console.error(error);
|
|
4194
4522
|
process.exit(1);
|
|
4195
4523
|
});
|