@tatara/cli 0.7.0 → 0.8.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 CHANGED
@@ -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,
@@ -235,19 +247,21 @@ async function writeIfChanged(path, content, written) {
235
247
  }
236
248
  async function sync(project = loadProject()) {
237
249
  const { root } = project;
250
+ const ejected = ejectedPackages(project.config);
251
+ const write = (path, content, out) => writeIfChanged(path, pointAtEjected(content, ejected), out);
238
252
  const out = join(root, "src/generated");
239
253
  await mkdir(out, { recursive: true });
240
254
  const written = [];
241
255
  const dirs = ["src/core/*", "src/modules/*"];
242
256
  const routes = await find(root, dirs.map((d) => `${d}/routes/*.routes.ts`));
243
- await writeIfChanged(join(out, "routes.ts"), listModule(routes, {
257
+ await write(join(out, "routes.ts"), listModule(routes, {
244
258
  prefix: "routes",
245
259
  typeImport: "import type { RouteDefinition } from \"#core/http/routes\";",
246
260
  typeName: "RouteDefinition",
247
261
  exportName: "routes"
248
262
  }), written);
249
263
  const commands = await find(root, dirs.map((d) => `${d}/commands/*.ts`));
250
- await writeIfChanged(join(out, "commands.ts"), listModule(commands, {
264
+ await write(join(out, "commands.ts"), listModule(commands, {
251
265
  prefix: "command",
252
266
  typeImport: "import type { CommandDefinition } from \"@tatara/core\";",
253
267
  typeName: "CommandDefinition",
@@ -255,14 +269,14 @@ async function sync(project = loadProject()) {
255
269
  }), written);
256
270
  if (project.has("jobs")) {
257
271
  const consumers = await find(root, dirs.map((d) => `${d}/consumers/*.ts`));
258
- await writeIfChanged(join(out, "consumers.ts"), listModule(consumers, {
272
+ await write(join(out, "consumers.ts"), listModule(consumers, {
259
273
  prefix: "consumer",
260
274
  typeImport: "import type { ConsumerDefinition } from \"@tatara/jobs\";",
261
275
  typeName: "ConsumerDefinition",
262
276
  exportName: "consumers"
263
277
  }), written);
264
278
  const schedules = await find(root, dirs.map((d) => `${d}/schedules/*.ts`));
265
- await writeIfChanged(join(out, "schedules.ts"), listModule(schedules, {
279
+ await write(join(out, "schedules.ts"), listModule(schedules, {
266
280
  prefix: "schedule",
267
281
  typeImport: "import type { ScheduleDefinition } from \"@tatara/jobs\";",
268
282
  typeName: "ScheduleDefinition",
@@ -273,7 +287,7 @@ async function sync(project = loadProject()) {
273
287
  const permissions = await find(root, ["src/modules/*/permissions/*.permissions.ts"]);
274
288
  const imports = permissions.map((f) => `import ${identifier(f, "permissions")} from "${specifier(f)}";`);
275
289
  const names = permissions.map((f) => identifier(f, "permissions"));
276
- await writeIfChanged(join(out, "permissions.ts"), `${HEADER}import { authzPermissions, mergeCatalogs, type PermissionKeysOf } from "@tatara/authz";
290
+ await write(join(out, "permissions.ts"), `${HEADER}import { authzPermissions, mergeCatalogs, type PermissionKeysOf } from "@tatara/authz";
277
291
  ${imports.join("\n")}${imports.length ? "\n" : ""}
278
292
  export const catalog = mergeCatalogs(authzPermissions${names.map((n) => `, ${n}`).join("")});
279
293
 
@@ -283,7 +297,7 @@ export type PermissionKey =
283
297
  }
284
298
  if (project.config.database !== "none") {
285
299
  const seeds = await find(root, dirs.map((d) => `${d}/db/seed.ts`));
286
- await writeIfChanged(join(out, "seeds.ts"), listModule(seeds, {
300
+ await write(join(out, "seeds.ts"), listModule(seeds, {
287
301
  prefix: "seed",
288
302
  typeImport: "import type { SeedDefinition } from \"@tatara/db\";",
289
303
  typeName: "SeedDefinition",
@@ -292,7 +306,7 @@ export type PermissionKey =
292
306
  }
293
307
  if (project.config.orm === "drizzle") {
294
308
  const schemas = await find(root, dirs.map((d) => `${d}/db/*.schema.ts`));
295
- await writeIfChanged(join(out, "schema.ts"), `${HEADER}${schemas.map((f) => `export * from "${specifier(f)}";`).join("\n")}\nexport {};\n`, written);
309
+ await write(join(out, "schema.ts"), `${HEADER}${schemas.map((f) => `export * from "${specifier(f)}";`).join("\n")}\nexport {};\n`, written);
296
310
  }
297
311
  if (project.config.orm === "prisma") {
298
312
  const bin = join(root, "node_modules/.bin/prisma");
@@ -650,6 +664,121 @@ function printDoctor(checks) {
650
664
  }
651
665
  }
652
666
  //#endregion
667
+ //#region src/commands/eject.ts
668
+ var EjectError = class extends Error {};
669
+ const short = (name) => name.replace(/^@tatara\//, "");
670
+ const full = (name) => `@tatara/${short(name)}`;
671
+ const NOT_LIBRARIES = /* @__PURE__ */ new Set(["cli", "create-tatara"]);
672
+ async function readJson(path) {
673
+ return JSON.parse(await readFile(path, "utf8"));
674
+ }
675
+ /**
676
+ * Copies a package's source into src/ejected/<name>, points the app's imports at it and records
677
+ * it in tatara.json. From then on the app owns that code and `tatara upgrade` leaves it alone.
678
+ */
679
+ async function eject(project, name, opts = {}) {
680
+ const target = short(name);
681
+ if (NOT_LIBRARIES.has(target)) throw new EjectError(`${full(target)} is a tool, not a library`);
682
+ const appPkgPath = join(project.root, "package.json");
683
+ const appPkg = await readJson(appPkgPath);
684
+ const installed = Object.keys({
685
+ ...appPkg.dependencies,
686
+ ...appPkg.devDependencies
687
+ }).filter((dep) => dep.startsWith("@tatara/") && !NOT_LIBRARIES.has(short(dep))).map(short);
688
+ const already = ejectedPackages(project.config);
689
+ if (already.includes(target)) throw new EjectError(`${full(target)} is already ejected`);
690
+ if (!installed.includes(target)) throw new EjectError(`${full(target)} is not a dependency of this app`);
691
+ const manifests = /* @__PURE__ */ new Map();
692
+ for (const dep of installed) {
693
+ const path = join(project.root, "node_modules", full(dep), "package.json");
694
+ if (existsSync(path)) manifests.set(dep, await readJson(path));
695
+ }
696
+ const chain = [target];
697
+ for (let i = 0; i < chain.length; i++) for (const [dep, manifest] of manifests) {
698
+ if (chain.includes(dep)) continue;
699
+ if (full(chain[i]) in (manifest.peerDependencies ?? {})) chain.push(dep);
700
+ }
701
+ const dependents = chain.slice(1);
702
+ if (dependents.length > 0 && !opts.withDependents) throw new EjectError(`${dependents.map(full).join(", ")} depend on ${full(target)}. Eject them too with --with-dependents.`);
703
+ const report = {
704
+ ejected: chain,
705
+ copied: [],
706
+ rewritten: [],
707
+ added: []
708
+ };
709
+ const versions = {};
710
+ for (const dep of chain) {
711
+ const manifest = manifests.get(dep);
712
+ const source = join(project.root, "node_modules", full(dep), "src");
713
+ 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`);
714
+ versions[dep] = manifest.version;
715
+ const files = await glob(["**/*.ts"], {
716
+ cwd: source,
717
+ ignore: ["**/*.test.ts"]
718
+ });
719
+ for (const file of files) {
720
+ const to = `src/ejected/${dep}/${file}`;
721
+ await mkdir(dirname(join(project.root, to)), { recursive: true });
722
+ await cp(join(source, file), join(project.root, to));
723
+ report.copied.push(to);
724
+ }
725
+ }
726
+ const ejected = [...already, ...chain];
727
+ for (const file of await glob(["src/**/*.ts", "tests/**/*.ts"], { cwd: project.root })) {
728
+ const path = join(project.root, file);
729
+ const text = await readFile(path, "utf8");
730
+ const next = pointAtEjected(text, ejected);
731
+ if (next !== text) {
732
+ await writeFile(path, next);
733
+ if (!report.copied.includes(file)) report.rewritten.push(file);
734
+ }
735
+ }
736
+ const deps = { ...appPkg.dependencies };
737
+ const listed = new Set(Object.keys({
738
+ ...appPkg.dependencies,
739
+ ...appPkg.devDependencies
740
+ }));
741
+ for (const dep of chain) {
742
+ const manifest = manifests.get(dep);
743
+ for (const [peer, range] of Object.entries(manifest.peerDependencies ?? {})) {
744
+ if (listed.has(peer) || manifest.peerDependenciesMeta?.[peer]?.optional) continue;
745
+ if (peer.startsWith("@tatara/") && ejected.includes(short(peer))) continue;
746
+ deps[peer] = range;
747
+ listed.add(peer);
748
+ report.added.push(peer);
749
+ }
750
+ }
751
+ for (const dep of chain) delete deps[full(dep)];
752
+ const devDeps = { ...appPkg.devDependencies };
753
+ for (const dep of chain) delete devDeps[full(dep)];
754
+ const nextPkg = {
755
+ ...appPkg,
756
+ imports: {
757
+ ...appPkg.imports,
758
+ "#ejected/*": "./src/ejected/*.ts"
759
+ },
760
+ dependencies: Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b))),
761
+ devDependencies: devDeps
762
+ };
763
+ await writeFile(appPkgPath, `${JSON.stringify(nextPkg, null, " ")}\n`);
764
+ const configPath = join(project.root, "tatara.json");
765
+ const config = await readJson(configPath);
766
+ await writeFile(configPath, `${JSON.stringify({
767
+ ...config,
768
+ ejected: {
769
+ ...config.ejected,
770
+ ...versions
771
+ }
772
+ }, null, " ")}\n`);
773
+ if (opts.formatFile) for (const file of [
774
+ ...report.copied,
775
+ ...report.rewritten,
776
+ "package.json",
777
+ "tatara.json"
778
+ ]) await opts.formatFile(file, project);
779
+ return report;
780
+ }
781
+ //#endregion
653
782
  //#region src/paths.ts
654
783
  /** Package root: one level above both src/ (dev) and dist/ (published, bundled). */
655
784
  const PACKAGE_ROOT = resolve(import.meta.dirname, "..");
@@ -2580,7 +2709,7 @@ async function generate(project, request) {
2580
2709
  for (const [rel, text] of rendered) {
2581
2710
  const full = join(project.root, rel);
2582
2711
  await mkdir(dirname(full), { recursive: true });
2583
- await writeFile(full, text);
2712
+ await writeFile(full, pointAtEjected(text, ejectedPackages(project.config)));
2584
2713
  }
2585
2714
  if (!request.skipPostProcess) await postProcess(project, files);
2586
2715
  return {
@@ -3953,7 +4082,9 @@ function createContext(project, baseline, opts = {}) {
3953
4082
  };
3954
4083
  const abs = (path) => join(project.root, path);
3955
4084
  let rendered;
3956
- const write = async (path, text) => {
4085
+ const ejected = ejectedPackages(project.config);
4086
+ const write = async (path, written) => {
4087
+ const text = path.endsWith(".ts") ? pointAtEjected(written, ejected) : written;
3957
4088
  if (!report.written.includes(path)) {
3958
4089
  report.written.push(path);
3959
4090
  const before = existsSync(abs(path)) ? await readFile(abs(path), "utf8") : void 0;
@@ -4014,6 +4145,11 @@ function createContext(project, baseline, opts = {}) {
4014
4145
  return (await rendered).get(path);
4015
4146
  },
4016
4147
  async addDependency(name, range, { dev = false } = {}) {
4148
+ if (ejected.includes(name.replace(/^@tatara\//, ""))) {
4149
+ const note = `${name} is ejected; it was not added (its code is in src/ejected)`;
4150
+ if (!report.notes.includes(note)) report.notes.push(note);
4151
+ return;
4152
+ }
4017
4153
  const pkg = JSON.parse(await readFile(abs("package.json"), "utf8"));
4018
4154
  const key = dev ? "devDependencies" : "dependencies";
4019
4155
  pkg[key] = {
@@ -4182,6 +4318,17 @@ program.command("upgrade").description("Run this CLI version's codemods on the a
4182
4318
  printUpgrade(await upgrade(project, { dryRun: flags.dryRun }), from, cliVersion());
4183
4319
  if (!flags.dryRun && flags.verify !== false && !await verify(loadProject())) process.exitCode = 1;
4184
4320
  });
4321
+ 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) => {
4322
+ const report = await eject(loadProject(), name, {
4323
+ withDependents: flags.withDependents,
4324
+ formatFile: biomeFormat
4325
+ });
4326
+ const names = report.ejected.map((n) => `@tatara/${n}`).join(", ");
4327
+ console.log(pc.green(`ejected ${names} into src/ejected (${report.copied.length} files)`));
4328
+ if (report.rewritten.length > 0) console.log(` ${report.rewritten.length} files now import the ejected copy`);
4329
+ if (report.added.length > 0) console.log(` added dependencies: ${report.added.join(", ")}`);
4330
+ console.log(` run your package manager's install, then verify; tatara upgrade skips ${names} from now on`);
4331
+ });
4185
4332
  program.command("doctor").description("Check the toolchain, services, env and layout").option("--json", "machine-readable output").action(async (flags) => {
4186
4333
  const checks = await doctor(loadProject());
4187
4334
  if (flags.json) console.log(JSON.stringify(checks, null, 2));
@@ -4189,7 +4336,7 @@ program.command("doctor").description("Check the toolchain, services, env and la
4189
4336
  if (checks.some((c) => !c.ok)) process.exitCode = 1;
4190
4337
  });
4191
4338
  program.parseAsync().catch((error) => {
4192
- if (error instanceof OptionsError || error instanceof NotATataraProject || error instanceof GeneratorError || error instanceof DbError) console.error(pc.red(error.message));
4339
+ if (error instanceof OptionsError || error instanceof EjectError || error instanceof NotATataraProject || error instanceof GeneratorError || error instanceof DbError) console.error(pc.red(error.message));
4193
4340
  else console.error(error);
4194
4341
  process.exit(1);
4195
4342
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tatara/cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Create and grow opinionated Hono backends",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,12 +31,12 @@
31
31
  "@aws-sdk/s3-request-presigner": "3.1152.0",
32
32
  "nodemailer": "10.1.1",
33
33
  "@types/nodemailer": "8.0.2",
34
- "@tatara/core": "0.1.0",
35
- "@tatara/http": "0.1.0",
36
- "@tatara/db": "0.1.0",
37
- "@tatara/testing": "0.1.0",
38
- "@tatara/auth": "0.1.0",
39
- "@tatara/keycloak": "0.1.0",
34
+ "@tatara/core": "0.1.1",
35
+ "@tatara/http": "0.1.1",
36
+ "@tatara/db": "0.1.1",
37
+ "@tatara/testing": "0.1.1",
38
+ "@tatara/auth": "0.1.1",
39
+ "@tatara/keycloak": "0.1.1",
40
40
  "@tatara/authz": "0.1.0",
41
41
  "@tatara/storage": "0.1.0",
42
42
  "@tatara/email": "0.1.0",