create-newt-app 0.35.1 → 0.35.3

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/index.js CHANGED
@@ -7,11 +7,11 @@ import { fileURLToPath } from "node:url";
7
7
  import { randomBytes } from "node:crypto";
8
8
  import { execa } from "execa";
9
9
  import ejs from "ejs";
10
- import { existsSync, promises } from "fs";
10
+ import { existsSync, promises, readdirSync } from "fs";
11
11
  import path from "path";
12
12
  import https from "node:https";
13
13
  //#region package.json
14
- var version = "0.35.1";
14
+ var version = "0.35.3";
15
15
  var engines = { "node": ">=24.0.0" };
16
16
  //#endregion
17
17
  //#region src/templates/root/index.ts
@@ -177,6 +177,21 @@ pnpm dev
177
177
  \`\`\`
178
178
 
179
179
  Open [http://localhost:3000](http://localhost:3000).
180
+ <% if (deployment === 'spa') { -%>
181
+
182
+ ## Production build
183
+
184
+ NestJS serves the static export and the API from one origin, so point \`BETTER_AUTH_URL\` at it:
185
+
186
+ \`\`\`sh
187
+ pnpm db:migrate
188
+ pnpm build
189
+ cd apps/api
190
+ BETTER_AUTH_URL=http://localhost:3001 pnpm start:prod
191
+ \`\`\`
192
+
193
+ Open [http://localhost:3001](http://localhost:3001). In a real deployment, set \`BETTER_AUTH_URL\` to the public URL.
194
+ <% } -%>
180
195
 
181
196
  ## Apps
182
197
 
@@ -921,6 +936,7 @@ const auth = { templates: [
921
936
  "name": "@<%= projectName %>/auth",
922
937
  "version": "0.0.0",
923
938
  "private": true,
939
+ "type": "module",
924
940
  "exports": {
925
941
  ".": "./src/index.ts"
926
942
  },
@@ -1022,7 +1038,6 @@ export interface SessionTable {
1022
1038
 
1023
1039
  export interface AccountTable {
1024
1040
  id: string;
1025
- issuer: string;
1026
1041
  accountId: string;
1027
1042
  providerId: string;
1028
1043
  userId: string;
@@ -1239,7 +1254,9 @@ export const db = new Kysely<DB>({
1239
1254
  dialect: new SqliteDialect({ database: driver }),
1240
1255
  });
1241
1256
 
1242
- export type Database = Kysely<DB>;`
1257
+ export type Database = Kysely<DB>;
1258
+
1259
+ export type * from "./schema.js";`
1243
1260
  };
1244
1261
  //#endregion
1245
1262
  //#region src/templates/db/templates/src-index-postgres.ts
@@ -1255,7 +1272,9 @@ export const db = new Kysely<DB>({
1255
1272
  dialect: new PostgresDialect({ pool: driver }),
1256
1273
  });
1257
1274
 
1258
- export type Database = Kysely<DB>;`
1275
+ export type Database = Kysely<DB>;
1276
+
1277
+ export type * from "./schema.js";`
1259
1278
  };
1260
1279
  //#endregion
1261
1280
  //#region src/templates/db/index.ts
@@ -12322,14 +12341,7 @@ var todos_service_default = {
12322
12341
  filename: "apps/api/src/todos/todos.service.ts",
12323
12342
  template: `import { Injectable, NotFoundException } from '@nestjs/common';
12324
12343
  import { randomUUID } from 'node:crypto';
12325
- import { db } from '@<%= projectName %>/db';
12326
-
12327
- export interface Todo {
12328
- id: string;
12329
- title: string;
12330
- done: boolean;
12331
- createdAt: string;
12332
- }
12344
+ import { db, type Todo } from '@<%= projectName %>/db';
12333
12345
 
12334
12346
  const columns = ['id', 'title', 'done', 'createdAt'] as const;
12335
12347
 
@@ -12428,6 +12440,15 @@ export interface TodoTable {
12428
12440
  createdAt: Generated<string>;
12429
12441
  }
12430
12442
 
12443
+ // Wire shape of a todo: what the api serves and the web app consumes.
12444
+ // The service converts table rows (raw sqlite values) into this.
12445
+ export interface Todo {
12446
+ id: string;
12447
+ title: string;
12448
+ done: boolean;
12449
+ createdAt: string;
12450
+ }
12451
+
12431
12452
  export interface UserTable {
12432
12453
  id: string;
12433
12454
  name: string;
@@ -12451,7 +12472,6 @@ export interface SessionTable {
12451
12472
 
12452
12473
  export interface AccountTable {
12453
12474
  id: string;
12454
- issuer: string;
12455
12475
  accountId: string;
12456
12476
  providerId: string;
12457
12477
  userId: string;
@@ -12619,7 +12639,7 @@ var api_index_default = {
12619
12639
  template: `export { AppModule } from './app.module';
12620
12640
  export { AppService } from './app.service';
12621
12641
  export { TodosService } from './todos/todos.service';
12622
- export type { Todo } from './todos/todos.service';`
12642
+ export type { Todo } from '@<%= projectName %>/db';`
12623
12643
  };
12624
12644
  //#endregion
12625
12645
  //#region src/templates/todo-example/templates/web-todos-route.ts
@@ -12711,25 +12731,28 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
12711
12731
  import { useForm } from '@tanstack/react-form';
12712
12732
  import { authClient } from '@/lib/auth-client';
12713
12733
  import { Button } from '@<%= projectName %>/ui/button';
12734
+ import type { Todo } from '@<%= projectName %>/db';
12714
12735
 
12715
- interface Todo {
12716
- id: string;
12717
- title: string;
12718
- done: boolean;
12736
+ async function request<T>(url: string, init?: RequestInit): Promise<T> {
12737
+ const res = await fetch(url, init);
12738
+ if (!res.ok) throw new Error('Request failed with status ' + res.status);
12739
+ const text = await res.text();
12740
+ // SAFETY: T is the response shape the api wrapper promises for each
12741
+ // endpoint, and every success path returns a JSON body.
12742
+ return (text ? JSON.parse(text) : undefined) as T;
12719
12743
  }
12720
12744
 
12721
12745
  const api = {
12722
- getTodos: (): Promise<Todo[]> => fetch('/api/todos').then((r) => r.json()),
12723
- createTodo: (title: string): Promise<Todo> =>
12724
- fetch('/api/todos', {
12746
+ getTodos: () => request<Todo[]>('/api/todos'),
12747
+ createTodo: (title: string) =>
12748
+ request<Todo>('/api/todos', {
12725
12749
  method: 'POST',
12726
12750
  headers: { 'Content-Type': 'application/json' },
12727
12751
  body: JSON.stringify({ title }),
12728
- }).then((r) => r.json()),
12729
- toggleTodo: (id: string): Promise<Todo> =>
12730
- fetch(\`/api/todos/\${id}/toggle\`, { method: 'PATCH' }).then((r) => r.json()),
12731
- deleteTodo: (id: string): Promise<void> =>
12732
- fetch(\`/api/todos/\${id}\`, { method: 'DELETE' }).then(() => undefined),
12752
+ }),
12753
+ toggleTodo: (id: string) =>
12754
+ request<Todo>(\`/api/todos/\${id}/toggle\`, { method: 'PATCH' }),
12755
+ deleteTodo: (id: string) => request<void>(\`/api/todos/\${id}\`, { method: 'DELETE' }),
12733
12756
  };
12734
12757
 
12735
12758
  export function TodoList({
@@ -12739,7 +12762,7 @@ export function TodoList({
12739
12762
  }) {
12740
12763
  const queryClient = useQueryClient();
12741
12764
 
12742
- const { data: todos = [], isPending } = useQuery({
12765
+ const { data: todos = [], isPending, isError } = useQuery({
12743
12766
  queryKey: ['todos'],
12744
12767
  queryFn: api.getTodos,
12745
12768
  });
@@ -12810,8 +12833,14 @@ export function TodoList({
12810
12833
  </form.Subscribe>
12811
12834
  </form>
12812
12835
 
12836
+ {(createMutation.isError || toggleMutation.isError || deleteMutation.isError) && (
12837
+ <p className="text-sm text-red-400 mb-4">Something went wrong. Try again.</p>
12838
+ )}
12839
+
12813
12840
  {isPending ? (
12814
12841
  <p className="text-sm text-muted-foreground">Loading…</p>
12842
+ ) : isError ? (
12843
+ <p className="text-sm text-red-400">Couldn&apos;t load todos.</p>
12815
12844
  ) : (
12816
12845
  <ul className="divide-y divide-neutral-700">
12817
12846
  {todos.map((todo) => (
@@ -12941,25 +12970,29 @@ import { authClient } from '@/lib/auth-client';
12941
12970
  import { Button } from '@<%= projectName %>/ui/button';
12942
12971
  import { Input } from '@<%= projectName %>/ui/input';
12943
12972
  import { Checkbox } from '@<%= projectName %>/ui/checkbox';
12973
+ import { toast } from 'sonner';
12974
+ import type { Todo } from '@<%= projectName %>/db';
12944
12975
 
12945
- interface Todo {
12946
- id: string;
12947
- title: string;
12948
- done: boolean;
12976
+ async function request<T>(url: string, init?: RequestInit): Promise<T> {
12977
+ const res = await fetch(url, init);
12978
+ if (!res.ok) throw new Error('Request failed with status ' + res.status);
12979
+ const text = await res.text();
12980
+ // SAFETY: T is the response shape the api wrapper promises for each
12981
+ // endpoint, and every success path returns a JSON body.
12982
+ return (text ? JSON.parse(text) : undefined) as T;
12949
12983
  }
12950
12984
 
12951
12985
  const api = {
12952
- getTodos: (): Promise<Todo[]> => fetch('/api/todos').then((r) => r.json()),
12953
- createTodo: (title: string): Promise<Todo> =>
12954
- fetch('/api/todos', {
12986
+ getTodos: () => request<Todo[]>('/api/todos'),
12987
+ createTodo: (title: string) =>
12988
+ request<Todo>('/api/todos', {
12955
12989
  method: 'POST',
12956
12990
  headers: { 'Content-Type': 'application/json' },
12957
12991
  body: JSON.stringify({ title }),
12958
- }).then((r) => r.json()),
12959
- toggleTodo: (id: string): Promise<Todo> =>
12960
- fetch(\`/api/todos/\${id}/toggle\`, { method: 'PATCH' }).then((r) => r.json()),
12961
- deleteTodo: (id: string): Promise<void> =>
12962
- fetch(\`/api/todos/\${id}\`, { method: 'DELETE' }).then(() => undefined),
12992
+ }),
12993
+ toggleTodo: (id: string) =>
12994
+ request<Todo>(\`/api/todos/\${id}/toggle\`, { method: 'PATCH' }),
12995
+ deleteTodo: (id: string) => request<void>(\`/api/todos/\${id}\`, { method: 'DELETE' }),
12963
12996
  };
12964
12997
 
12965
12998
  export function TodoList({
@@ -12969,7 +13002,7 @@ export function TodoList({
12969
13002
  }) {
12970
13003
  const queryClient = useQueryClient();
12971
13004
 
12972
- const { data: todos = [], isPending } = useQuery({
13005
+ const { data: todos = [], isPending, isError } = useQuery({
12973
13006
  queryKey: ['todos'],
12974
13007
  queryFn: api.getTodos,
12975
13008
  });
@@ -12977,16 +13010,19 @@ export function TodoList({
12977
13010
  const createMutation = useMutation({
12978
13011
  mutationFn: api.createTodo,
12979
13012
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
13013
+ onError: () => toast.error("Couldn't add todo"),
12980
13014
  });
12981
13015
 
12982
13016
  const toggleMutation = useMutation({
12983
13017
  mutationFn: api.toggleTodo,
12984
13018
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
13019
+ onError: () => toast.error("Couldn't update todo"),
12985
13020
  });
12986
13021
 
12987
13022
  const deleteMutation = useMutation({
12988
13023
  mutationFn: api.deleteTodo,
12989
13024
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
13025
+ onError: () => toast.error("Couldn't delete todo"),
12990
13026
  });
12991
13027
 
12992
13028
  const form = useForm({
@@ -13043,6 +13079,8 @@ export function TodoList({
13043
13079
 
13044
13080
  {isPending ? (
13045
13081
  <p className="text-sm text-muted-foreground">Loading…</p>
13082
+ ) : isError ? (
13083
+ <p className="text-sm text-destructive">Couldn&apos;t load todos.</p>
13046
13084
  ) : (
13047
13085
  <ul className="divide-y divide-border">
13048
13086
  {todos.map((todo) => (
@@ -13195,25 +13233,28 @@ import * as stylex from '@stylexjs/stylex';
13195
13233
  import { authClient } from '@/lib/auth-client';
13196
13234
  import { Button } from '@<%= projectName %>/ui/button';
13197
13235
  import { colors, radii } from '@<%= projectName %>/ui/tokens.stylex';
13236
+ import type { Todo } from '@<%= projectName %>/db';
13198
13237
 
13199
- interface Todo {
13200
- id: string;
13201
- title: string;
13202
- done: boolean;
13238
+ async function request<T>(url: string, init?: RequestInit): Promise<T> {
13239
+ const res = await fetch(url, init);
13240
+ if (!res.ok) throw new Error('Request failed with status ' + res.status);
13241
+ const text = await res.text();
13242
+ // SAFETY: T is the response shape the api wrapper promises for each
13243
+ // endpoint, and every success path returns a JSON body.
13244
+ return (text ? JSON.parse(text) : undefined) as T;
13203
13245
  }
13204
13246
 
13205
13247
  const api = {
13206
- getTodos: (): Promise<Todo[]> => fetch('/api/todos').then((r) => r.json()),
13207
- createTodo: (title: string): Promise<Todo> =>
13208
- fetch('/api/todos', {
13248
+ getTodos: () => request<Todo[]>('/api/todos'),
13249
+ createTodo: (title: string) =>
13250
+ request<Todo>('/api/todos', {
13209
13251
  method: 'POST',
13210
13252
  headers: { 'Content-Type': 'application/json' },
13211
13253
  body: JSON.stringify({ title }),
13212
- }).then((r) => r.json()),
13213
- toggleTodo: (id: string): Promise<Todo> =>
13214
- fetch(\`/api/todos/\${id}/toggle\`, { method: 'PATCH' }).then((r) => r.json()),
13215
- deleteTodo: (id: string): Promise<void> =>
13216
- fetch(\`/api/todos/\${id}\`, { method: 'DELETE' }).then(() => undefined),
13254
+ }),
13255
+ toggleTodo: (id: string) =>
13256
+ request<Todo>(\`/api/todos/\${id}/toggle\`, { method: 'PATCH' }),
13257
+ deleteTodo: (id: string) => request<void>(\`/api/todos/\${id}\`, { method: 'DELETE' }),
13217
13258
  };
13218
13259
 
13219
13260
  export function TodoList({
@@ -13223,7 +13264,7 @@ export function TodoList({
13223
13264
  }) {
13224
13265
  const queryClient = useQueryClient();
13225
13266
 
13226
- const { data: todos = [], isPending } = useQuery({
13267
+ const { data: todos = [], isPending, isError } = useQuery({
13227
13268
  queryKey: ['todos'],
13228
13269
  queryFn: api.getTodos,
13229
13270
  });
@@ -13294,8 +13335,14 @@ export function TodoList({
13294
13335
  </form.Subscribe>
13295
13336
  </form>
13296
13337
 
13338
+ {(createMutation.isError || toggleMutation.isError || deleteMutation.isError) && (
13339
+ <p {...stylex.props(styles.error)}>Something went wrong. Try again.</p>
13340
+ )}
13341
+
13297
13342
  {isPending ? (
13298
13343
  <p {...stylex.props(styles.muted)}>Loading…</p>
13344
+ ) : isError ? (
13345
+ <p {...stylex.props(styles.error)}>Couldn&apos;t load todos.</p>
13299
13346
  ) : (
13300
13347
  <ul {...stylex.props(styles.list)}>
13301
13348
  {todos.map((todo) => (
@@ -13420,6 +13467,11 @@ const styles = stylex.create({
13420
13467
  fontSize: '0.875rem',
13421
13468
  color: colors.mutedForeground,
13422
13469
  },
13470
+ error: {
13471
+ fontSize: '0.875rem',
13472
+ color: colors.danger,
13473
+ marginBottom: '1rem',
13474
+ },
13423
13475
  });
13424
13476
  `
13425
13477
  };
@@ -13629,9 +13681,23 @@ const todoExampleDi = { templates: [
13629
13681
  app_module_di_default,
13630
13682
  api_index_default
13631
13683
  ] };
13632
- const todoExampleWeb = { templates: [web_todo_list_default, web_page_default] };
13633
- const todoExampleShadcn = { templates: [shadcn_todo_list_default, shadcn_page_default] };
13634
- const todoExampleStylex = { templates: [stylex_todo_list_default, stylex_page_default] };
13684
+ const webDbPackage = {
13685
+ package: "@<%= projectName %>/db",
13686
+ module: "apps/web",
13687
+ version: "workspace:*"
13688
+ };
13689
+ const todoExampleWeb = {
13690
+ templates: [web_todo_list_default, web_page_default],
13691
+ packages: [webDbPackage]
13692
+ };
13693
+ const todoExampleShadcn = {
13694
+ templates: [shadcn_todo_list_default, shadcn_page_default],
13695
+ packages: [webDbPackage]
13696
+ };
13697
+ const todoExampleStylex = {
13698
+ templates: [stylex_todo_list_default, stylex_page_default],
13699
+ packages: [webDbPackage]
13700
+ };
13635
13701
  //#endregion
13636
13702
  //#region src/templates/index.ts
13637
13703
  const staticDir = new URL("./static/", import.meta.url);
@@ -13831,29 +13897,43 @@ function validateNodeVersion(current, requirement) {
13831
13897
  };
13832
13898
  }
13833
13899
  function normalizeProjectName(projectName) {
13834
- return projectName.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9\-._~]/g, "-").replace(/-+/g, "-").replace(/^[-._]+/, "").replace(/-+$/, "").slice(0, 214);
13900
+ return projectName.trim().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9\-._~]/g, "-").replace(/-+/g, "-").replace(/^[-._]+/, "").replace(/-+$/, "").slice(0, 214);
13901
+ }
13902
+ function projectTarget(projectName) {
13903
+ if ([".", "./"].includes(projectName.trim())) return {
13904
+ name: normalizeProjectName(path.basename(process.cwd())),
13905
+ dir: "."
13906
+ };
13907
+ const name = normalizeProjectName(projectName);
13908
+ return {
13909
+ name,
13910
+ dir: name
13911
+ };
13835
13912
  }
13836
13913
  function validateProjectName(projectName) {
13837
13914
  if (!projectName) return {
13838
13915
  valid: false,
13839
13916
  error: "Project name is required"
13840
13917
  };
13841
- const normalized = normalizeProjectName(projectName);
13842
- if (!normalized) return {
13918
+ const { name, dir } = projectTarget(projectName);
13919
+ if (!name) return {
13843
13920
  valid: false,
13844
13921
  error: `"${projectName}" has nothing left after normalizing. Include a letter or a digit.`
13845
13922
  };
13846
- const targetPath = path.resolve(process.cwd(), normalized);
13847
- if (existsSync(targetPath)) return {
13923
+ if (dir === ".") return readdirSync(process.cwd()).length === 0 ? { valid: true } : {
13848
13924
  valid: false,
13849
- error: `Directory "${normalized}" already exists`
13925
+ error: "The current directory is not empty"
13926
+ };
13927
+ if (existsSync(path.resolve(process.cwd(), dir))) return {
13928
+ valid: false,
13929
+ error: `Directory "${dir}" already exists`
13850
13930
  };
13851
13931
  return { valid: true };
13852
13932
  }
13853
13933
  //#endregion
13854
13934
  //#region src/tasks.ts
13855
13935
  async function scaffold(modules, options) {
13856
- const validation = validateProjectName(options.name);
13936
+ const validation = validateProjectName(options.dir);
13857
13937
  if (!validation.valid) {
13858
13938
  console.error(`Error: ${validation.error}`);
13859
13939
  process.exit(1);
@@ -13870,12 +13950,12 @@ async function scaffold(modules, options) {
13870
13950
  authSecret: randomBytes(32).toString("base64url"),
13871
13951
  versions
13872
13952
  };
13873
- await renderTemplatesToDisk(modules, options.name, templateData, options.selection);
13953
+ await renderTemplatesToDisk(modules, options.dir, templateData, options.selection);
13874
13954
  const packages = modules.map((mod) => mod.packages).filter((ele) => ele !== void 0).flat();
13875
- if (packages.length > 0) await updatePackageJson(options.name, packages, templateData);
13955
+ if (packages.length > 0) await updatePackageJson(options.dir, packages, templateData);
13876
13956
  const scripts = modules.map((mod) => mod.scripts).filter((ele) => ele !== void 0).flat();
13877
- if (scripts.length > 0) await updateScripts(options.name, scripts, templateData);
13878
- await sortPackageJsons(options.name);
13957
+ if (scripts.length > 0) await updateScripts(options.dir, scripts, templateData);
13958
+ await sortPackageJsons(options.dir);
13879
13959
  }
13880
13960
  async function hasCommand(command) {
13881
13961
  try {
@@ -14163,13 +14243,14 @@ async function doInit(options) {
14163
14243
  const rawName = answers.name ?? options.name ?? "";
14164
14244
  const nameCheck = validateProjectName(rawName);
14165
14245
  if (!nameCheck.valid) throw new Error(nameCheck.error);
14166
- const name = normalizeProjectName(rawName);
14246
+ const { name, dir } = projectTarget(rawName);
14167
14247
  const taskBuilder = new TaskBuilder();
14168
14248
  taskBuilder.add({
14169
14249
  title: "Scaffolding project",
14170
14250
  task: async () => {
14171
14251
  await scaffold(allModules, {
14172
14252
  name,
14253
+ dir,
14173
14254
  testing,
14174
14255
  database,
14175
14256
  deployment,
@@ -14184,14 +14265,14 @@ async function doInit(options) {
14184
14265
  taskBuilder.add({
14185
14266
  title: "Installing with pnpm",
14186
14267
  task: async () => {
14187
- await pnpmInstall(name);
14268
+ await pnpmInstall(dir);
14188
14269
  return "Installed.";
14189
14270
  }
14190
14271
  });
14191
14272
  taskBuilder.add({
14192
14273
  title: "Formatting",
14193
14274
  task: async () => {
14194
- await pnpmFormat(name);
14275
+ await pnpmFormat(dir);
14195
14276
  return "Formatted.";
14196
14277
  }
14197
14278
  });
@@ -14200,15 +14281,19 @@ async function doInit(options) {
14200
14281
  if (options.git) taskBuilder.add({
14201
14282
  title: "Initializing git",
14202
14283
  task: async () => {
14203
- committed = await initGit(name);
14284
+ committed = await initGit(dir);
14204
14285
  return committed ? "Initialized git." : "Initialized git, but could not commit.";
14205
14286
  }
14206
14287
  });
14207
14288
  await p.tasks(taskBuilder.tasks);
14208
14289
  p.outro(`Done!`);
14290
+ if (dir !== "." && dir !== rawName) {
14291
+ console.log(`Note: the project directory is "${dir}", not "${rawName}".`);
14292
+ console.log();
14293
+ }
14209
14294
  console.log("Next steps:");
14210
14295
  console.log();
14211
- console.log(chalk.blue(` cd ${name}`));
14296
+ if (dir !== ".") console.log(chalk.blue(` cd ${dir}`));
14212
14297
  if (!options.install) {
14213
14298
  console.log(chalk.blue(` pnpm install`));
14214
14299
  console.log(chalk.blue(` pnpm format:fix`));