create-newt-app 0.35.1 → 0.35.2

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
@@ -11,7 +11,7 @@ import { existsSync, promises } 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.2";
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,7 +13897,7 @@ 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);
13835
13901
  }
13836
13902
  function validateProjectName(projectName) {
13837
13903
  if (!projectName) return {
@@ -14206,6 +14272,10 @@ async function doInit(options) {
14206
14272
  });
14207
14273
  await p.tasks(taskBuilder.tasks);
14208
14274
  p.outro(`Done!`);
14275
+ if (name !== rawName) {
14276
+ console.log(`Note: the project directory is "${name}", not "${rawName}".`);
14277
+ console.log();
14278
+ }
14209
14279
  console.log("Next steps:");
14210
14280
  console.log();
14211
14281
  console.log(chalk.blue(` cd ${name}`));