diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 2b9b15ea..fe6575e1 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -9,7 +9,7 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - - run: corepack enable + - run: npm install -g corepack@latest && corepack enable - uses: actions/setup-node@v4 with: node-version: 20 diff --git a/apps/site/.env.example b/apps/site/.env.example index 71272a7e..3d7dacfa 100644 --- a/apps/site/.env.example +++ b/apps/site/.env.example @@ -1,3 +1,4 @@ DOCSEARCH_APP_ID=RETR9S9VHS DOCSEARCH_API_KEY=326c1723a310dfe29004b47608709907 DOCSEARCH_INDEX_NAME=tailwindui-protocol +GITHUB_TOKEN=foo diff --git a/apps/site/app/root.tsx b/apps/site/app/root.tsx index 31dfb615..355273a1 100644 --- a/apps/site/app/root.tsx +++ b/apps/site/app/root.tsx @@ -1,5 +1,4 @@ import { - json, type LinksFunction, type MetaFunction, type LoaderFunctionArgs, @@ -46,13 +45,13 @@ export const links: LinksFunction = () => [ ]; export async function loader({ context: { cloudflare } }: LoaderFunctionArgs) { - return json({ + return { ENV: { DOCSEARCH_APP_ID: cloudflare.env.DOCSEARCH_APP_ID, DOCSEARCH_API_KEY: cloudflare.env.DOCSEARCH_API_KEY, DOCSEARCH_INDEX_NAME: cloudflare.env.DOCSEARCH_INDEX_NAME, }, - }); + }; } const themeScript = ` diff --git a/apps/site/app/routes/_index.tsx b/apps/site/app/routes/_index.tsx index 6be01456..a8f01ebf 100644 --- a/apps/site/app/routes/_index.tsx +++ b/apps/site/app/routes/_index.tsx @@ -1,5 +1,4 @@ import { - json, type LoaderFunctionArgs, type MetaFunction, } from "@remix-run/cloudflare"; @@ -34,7 +33,7 @@ export async function loader({ const { content, title, tableOfContents, description } = parseMarkdoc(markdown); - return json({ content, title, tableOfContents, manifest, description }); + return { content, title, tableOfContents, manifest, description }; } export const meta: MetaFunction = ({ data }) => [ diff --git a/apps/site/types/build.d.ts b/apps/site/types/build.d.ts deleted file mode 100644 index 7744b692..00000000 --- a/apps/site/types/build.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type ServerBuild } from "@remix-run/cloudflare"; - -export const assets: ServerBuild["assets"]; -export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"]; -export const entry: ServerBuild["entry"]; -export const future: ServerBuild["future"]; -export const publicPath: ServerBuild["publicPath"]; -export const routes: ServerBuild["routes"]; diff --git a/apps/site/vite.config.ts b/apps/site/vite.config.ts index eaa5f35a..2d815fce 100644 --- a/apps/site/vite.config.ts +++ b/apps/site/vite.config.ts @@ -5,6 +5,13 @@ import { } from "@remix-run/dev"; import tsconfigPaths from "vite-tsconfig-paths"; +declare module "@remix-run/cloudflare" { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Future { + v3_singleFetch: true; + } +} + export default defineConfig({ plugins: [ cloudflareDevProxyVitePlugin(), @@ -13,6 +20,7 @@ export default defineConfig({ v3_fetcherPersist: true, v3_lazyRouteDiscovery: true, v3_relativeSplatPath: true, + v3_singleFetch: true, v3_throwAbortReason: true, }, }), diff --git a/apps/site/worker-configuration.d.ts b/apps/site/worker-configuration.d.ts index dbd8675b..ec78ae4b 100644 --- a/apps/site/worker-configuration.d.ts +++ b/apps/site/worker-configuration.d.ts @@ -4,4 +4,5 @@ interface Env { DOCSEARCH_APP_ID: string; DOCSEARCH_API_KEY: string; DOCSEARCH_INDEX_NAME: string; + GITHUB_TOKEN: string; } diff --git a/examples/remix-cms/app/models/Article.ts b/examples/remix-cms/app/models/Article.ts index 943c80ed..74fdc7fa 100644 --- a/examples/remix-cms/app/models/Article.ts +++ b/examples/remix-cms/app/models/Article.ts @@ -2,7 +2,7 @@ import { Model } from "superflare"; import { User } from "./User"; export class Article extends Model { - user!: User | Promise; + user!: User; $user() { return this.belongsTo(User); } diff --git a/examples/remix-cms/app/models/User.ts b/examples/remix-cms/app/models/User.ts index 95a280e9..859ec91a 100644 --- a/examples/remix-cms/app/models/User.ts +++ b/examples/remix-cms/app/models/User.ts @@ -6,6 +6,7 @@ export class User extends Model { return rest; } } + Model.register(User); export interface User extends UserRow {} diff --git a/examples/remix-cms/app/routes/admin.tsx b/examples/remix-cms/app/routes/admin.tsx index 593cf6d8..bfc68ffe 100644 --- a/examples/remix-cms/app/routes/admin.tsx +++ b/examples/remix-cms/app/routes/admin.tsx @@ -14,7 +14,7 @@ import { import clsx from "clsx"; import { Link, NavLink, Outlet, useLoaderData } from "@remix-run/react"; import { Toast } from "~/components/Toast"; -import { json, type LoaderFunctionArgs, redirect } from "@remix-run/cloudflare"; +import { type LoaderFunctionArgs, redirect } from "@remix-run/cloudflare"; import { User } from "~/models/User"; const navigation = [ @@ -33,10 +33,7 @@ export async function loader({ const user = await auth.user(User); - return json({ - flash, - user, - }); + return { flash, user }; } export default function AdminLayout() { diff --git a/examples/remix-cms/app/routes/admin/articles.$slug.preview.tsx b/examples/remix-cms/app/routes/admin/articles.$slug.preview.tsx index b4edd183..8570d78d 100644 --- a/examples/remix-cms/app/routes/admin/articles.$slug.preview.tsx +++ b/examples/remix-cms/app/routes/admin/articles.$slug.preview.tsx @@ -19,10 +19,10 @@ export async function loader({ params }: LoaderFunctionArgs) { throw new Response("Not found", { status: 404 }); } - return json({ + return { article, html: await convertToHtml(article.content ?? ""), - }); + }; } export default function NewArticle() { diff --git a/examples/remix-cms/app/routes/admin/articles.$slug.tsx b/examples/remix-cms/app/routes/admin/articles.$slug.tsx index f7732034..b9d4e2a2 100644 --- a/examples/remix-cms/app/routes/admin/articles.$slug.tsx +++ b/examples/remix-cms/app/routes/admin/articles.$slug.tsx @@ -1,5 +1,5 @@ import { EyeIcon } from "@heroicons/react/24/outline"; -import { json, type LoaderFunctionArgs } from "@remix-run/cloudflare"; +import { type LoaderFunctionArgs } from "@remix-run/cloudflare"; import { useLoaderData, useRevalidator } from "@remix-run/react"; import invariant from "tiny-invariant"; import { Button, SecondaryButton } from "~/components/admin/Button"; @@ -24,7 +24,7 @@ export async function loader({ params }: LoaderFunctionArgs) { SayHelloJob.dispatch(article); - return json({ article }); + return { article }; } export default function NewArticle() { diff --git a/examples/remix-cms/app/routes/admin/articles.tsx b/examples/remix-cms/app/routes/admin/articles.tsx index 91c6ae79..75dd6515 100644 --- a/examples/remix-cms/app/routes/admin/articles.tsx +++ b/examples/remix-cms/app/routes/admin/articles.tsx @@ -1,4 +1,3 @@ -import { json } from "@remix-run/cloudflare"; import { Link, useLoaderData } from "@remix-run/react"; import { Button } from "~/components/admin/Button"; import { Page } from "~/components/admin/Page"; @@ -8,7 +7,7 @@ import { useChannel } from "~/utils/use-channel"; export async function loader() { const articles = await Article.with("user").orderBy("createdAt", "desc"); - return json({ articles }); + return { articles }; } export default function Articles() { diff --git a/examples/remix-cms/app/routes/admin/components/article-form.tsx b/examples/remix-cms/app/routes/admin/components/article-form.tsx index 19fde31d..879bfd55 100644 --- a/examples/remix-cms/app/routes/admin/components/article-form.tsx +++ b/examples/remix-cms/app/routes/admin/components/article-form.tsx @@ -1,9 +1,4 @@ -import { - json, - redirect, - type SerializeFrom, - type ActionFunctionArgs, -} from "@remix-run/cloudflare"; +import { redirect, type ActionFunctionArgs } from "@remix-run/cloudflare"; import { Form, useActionData } from "@remix-run/react"; import { Article } from "~/models/Article"; import invariant from "tiny-invariant"; @@ -29,7 +24,7 @@ const enum Intent { Update = "update", } -const badResponse = (data: ActionData) => json(data, { status: 422 }); +const badResponse = (data: ActionData) => Response.json(data, { status: 422 }); export async function action({ request, @@ -110,7 +105,7 @@ export function ArticleForm({ article, id, }: { - article?: SerializeFrom
; + article?: Article; id?: string; }) { const actionData = useActionData(); diff --git a/examples/remix-cms/app/routes/admin/upload.$.ts b/examples/remix-cms/app/routes/admin/upload.$.ts index 66a572e1..60994ba7 100644 --- a/examples/remix-cms/app/routes/admin/upload.$.ts +++ b/examples/remix-cms/app/routes/admin/upload.$.ts @@ -1,4 +1,4 @@ -import { json, type ActionFunctionArgs } from "@remix-run/cloudflare"; +import { type ActionFunctionArgs } from "@remix-run/cloudflare"; import { parseMultipartFormData, storage } from "superflare"; export async function action({ request }: ActionFunctionArgs) { @@ -13,7 +13,5 @@ export async function action({ request }: ActionFunctionArgs) { } ); - return json({ - url: storage().url(formData.get("file") as string), - }); + return { url: storage().url(formData.get("file") as string) }; } diff --git a/examples/remix-cms/app/routes/auth/login.tsx b/examples/remix-cms/app/routes/auth/login.tsx index bb3b5124..397c45e8 100644 --- a/examples/remix-cms/app/routes/auth/login.tsx +++ b/examples/remix-cms/app/routes/auth/login.tsx @@ -1,5 +1,5 @@ +import { redirect, type ActionFunctionArgs } from "@remix-run/cloudflare"; import { Form, Link, useActionData } from "@remix-run/react"; -import { json, redirect, type ActionFunctionArgs } from "@remix-run/cloudflare"; import { Button } from "~/components/admin/Button"; import { FormField } from "~/components/Form"; import { User } from "~/models/User"; @@ -26,11 +26,11 @@ export async function action({ return redirect("/admin"); } - return json({ error: "Invalid credentials" }, { status: 400 }); + return Response.json({ error: "Invalid credentials" }, { status: 400 }); } export default function Login() { - const actionData = useActionData(); + const actionData = useActionData<{ error: string }>(); return ( <> diff --git a/examples/remix-cms/app/routes/auth/register.tsx b/examples/remix-cms/app/routes/auth/register.tsx index 3ccc8ebc..341b8a79 100644 --- a/examples/remix-cms/app/routes/auth/register.tsx +++ b/examples/remix-cms/app/routes/auth/register.tsx @@ -1,5 +1,5 @@ import { Form, Link, useActionData } from "@remix-run/react"; -import { json, redirect, type ActionFunctionArgs } from "@remix-run/cloudflare"; +import { redirect, type ActionFunctionArgs } from "@remix-run/cloudflare"; import { Button } from "~/components/admin/Button"; import { FormField } from "~/components/Form"; import { User } from "~/models/User"; @@ -19,7 +19,7 @@ export async function action({ const name = formData.get("name") as string; if (await User.where("email", email).count()) { - return json({ error: "Email already exists" }, { status: 400 }); + return Response.json({ error: "Email already exists" }, { status: 400 }); } const user = await User.create({ @@ -34,7 +34,7 @@ export async function action({ } export default function Register() { - const actionData = useActionData(); + const actionData = useActionData<{ error: string }>(); return (
diff --git a/examples/remix-cms/types/build.d.ts b/examples/remix-cms/types/build.d.ts deleted file mode 100644 index 7744b692..00000000 --- a/examples/remix-cms/types/build.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type ServerBuild } from "@remix-run/cloudflare"; - -export const assets: ServerBuild["assets"]; -export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"]; -export const entry: ServerBuild["entry"]; -export const future: ServerBuild["future"]; -export const publicPath: ServerBuild["publicPath"]; -export const routes: ServerBuild["routes"]; diff --git a/examples/remix-cms/vite.config.ts b/examples/remix-cms/vite.config.ts index eee688b5..a51c140d 100644 --- a/examples/remix-cms/vite.config.ts +++ b/examples/remix-cms/vite.config.ts @@ -4,6 +4,13 @@ import { createRoutesFromFolders } from "@remix-run/v1-route-convention"; import { superflareDevProxyVitePlugin } from "@superflare/remix/dev"; import tsconfigPaths from "vite-tsconfig-paths"; +declare module "@remix-run/cloudflare" { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Future { + v3_singleFetch: true; + } +} + export default defineConfig({ plugins: [ superflareDevProxyVitePlugin(), @@ -12,6 +19,7 @@ export default defineConfig({ v3_fetcherPersist: true, v3_lazyRouteDiscovery: true, v3_relativeSplatPath: true, + v3_singleFetch: true, v3_throwAbortReason: true, }, // Tell Remix to ignore everything in the routes directory. diff --git a/packages/superflare/docs/database/getting-started.md b/packages/superflare/docs/database/getting-started.md index 1d75a9bf..68f4d946 100644 --- a/packages/superflare/docs/database/getting-started.md +++ b/packages/superflare/docs/database/getting-started.md @@ -58,7 +58,7 @@ However, we cannot simply send the entire data model over the wire from the serv Instead, modern frameworks will serialize the output sent (from "loaders" in Remix, or passed as props from server components to client components in Next.js). -Superflare makes it obvious how to serialize your models as JSON by providing the standard `toJSON()` method by on new models. +Superflare makes it obvious how to serialize your models as JSON by providing the standard `toJSON()` method on new models. By default, this method will return all of the model's attributes, in addition to any relations that are loaded on the instance either manually or by using eager-loading. diff --git a/packages/superflare/docs/database/relationships.md b/packages/superflare/docs/database/relationships.md index 768ee497..18335b4b 100644 --- a/packages/superflare/docs/database/relationships.md +++ b/packages/superflare/docs/database/relationships.md @@ -18,7 +18,7 @@ import { Model } from "superflare"; import { Profile } from "./Profile"; export class User extends Model { - profile!: Profile | Promise; + profile!: Profile; $profile() { return this.hasOne(Profile); } @@ -46,7 +46,7 @@ To define the inverse relationship, use the `belongsTo` method: import { Model } from "superflare"; export class Profile extends Model { - user!: User | Promise; + user!: User; $user() { return this.belongsTo(User); } @@ -71,7 +71,7 @@ To define a one-to-many relationship, use the `hasMany` method: import { Model } from "superflare"; export class User extends Model { - posts!: Post[] | Promise; + posts!: Post[]; $posts() { return this.hasMany(Post); } @@ -96,7 +96,7 @@ To define the inverse relationship, use the `belongsTo` method: import { Model } from "superflare"; export class Post extends Model { - user!: User | Promise; + user!: User; $user() { return this.belongsTo(User); } @@ -144,7 +144,7 @@ To define a many-to-many relationship, use the `belongsToMany` method: import { Model } from "superflare"; export class Post extends Model { - tags!: Tag[] | Promise; + tags!: Tag[]; $tags() { return this.belongsToMany(Tag); } @@ -163,7 +163,7 @@ for (const tag of tags) { } ``` -When invoking the `$tags()` method or awaiting the `tags` property, Superflare will check an intermediate table, often referred to as a "join table," for rows which have a `postId` that matches the `id` of the `Post` model. It will return all `Tag` models which have a `id` that matches the `tagId` of the join table rows. +When invoking the `$tags()` method or awaiting the `tags` property, Superflare will check an intermediate table, often referred to as a "join table", for rows which have a `postId` that matches the `id` of the `Post` model. It will return all `Tag` models which have a `id` that matches the `tagId` of the join table rows. To define the inverse relationship, use the `belongsToMany` method again: @@ -171,7 +171,7 @@ To define the inverse relationship, use the `belongsToMany` method again: import { Model } from "superflare"; export class Tag extends Model { - posts!: Post[] | Promise; + posts!: Post[]; $posts() { return this.belongsToMany(Post); } @@ -202,7 +202,6 @@ If you don't eager load the related models, the related data will not be availab ```tsx import { User } from "~/models/User"; -import { json } from "@remix-run/cloudflare"; export async function loader() { // ❌ The profile relation will not be loaded @@ -211,7 +210,7 @@ export async function loader() { // ✅ This will load the profile relation for the view const user = await User.with("profile").find(1); - return json({ user }); + return { user }; } export default function UserView() { diff --git a/packages/superflare/docs/security/authentication.md b/packages/superflare/docs/security/authentication.md index 5a9f72cd..ab9d38cc 100644 --- a/packages/superflare/docs/security/authentication.md +++ b/packages/superflare/docs/security/authentication.md @@ -43,7 +43,7 @@ export async function loader({ context: { auth } }: LoaderFunctionArgs) { const user = await auth.user(User); // If the user is logged in, show them the secret page - return json({ message: `You're logged in, ${user.name}!` }); + return { message: `You're logged in, ${user.name}!` }; } ``` diff --git a/packages/superflare/docs/sessions.md b/packages/superflare/docs/sessions.md index 0fa43068..4eb1fa5f 100644 --- a/packages/superflare/docs/sessions.md +++ b/packages/superflare/docs/sessions.md @@ -27,7 +27,7 @@ export async function action({ context: { session } }) { export async function loader({ context: { session } }) { const theme = session.get("theme"); - return json({ theme }); + return { theme }; } ``` @@ -41,7 +41,7 @@ To do this, you can use the `flash` method: export async function loader({ context: { session } }) { session.flash("success", "Your form was submitted successfully!"); - return json({ success: true }); + return { success: true }; } ``` @@ -51,7 +51,7 @@ Then, you can read the flash message in your action using the `getFlash` method: export async function action({ context: { session } }) { const success = session.getFlash("success"); - return json({ success }); + return { success }; } ``` diff --git a/packages/superflare/index.types.ts b/packages/superflare/index.types.ts index 88720139..171d38b0 100644 --- a/packages/superflare/index.types.ts +++ b/packages/superflare/index.types.ts @@ -69,18 +69,12 @@ export interface HasMany[]> * Shape of the model constructor (static properties). */ export interface BaseModel { - find(this: T, ids: number[]): Promise[]>; + find(this: T, ids: number[]): QueryBuilder; find( this: T, id: number - ): Promise>; - first(this: T): Promise>; - orderBy( - this: T, - field: string, - direction?: "asc" | "desc" - ): QueryBuilder; - all(this: T): Promise[]>; + ): QueryBuilder, true>; + all(this: T): QueryBuilder; where( this: T, field: string, @@ -101,12 +95,21 @@ export interface BaseModel { this: T, relationName: string | string[] ): QueryBuilder; + orderBy( + this: T, + field: string, + direction?: "asc" | "desc" + ): QueryBuilder; + query(this: T): QueryBuilder; + // Changes return type to single instance: + first(this: T): QueryBuilder, true>; + count(this: T): Promise; + create( this: T, attributes: any ): Promise>; - count(this: T): Promise; - query(this: T): QueryBuilder; + register(model: any): void; tableName: string; @@ -115,20 +118,46 @@ export interface BaseModel { new (attributes?: any): ModelInstance; } -interface QueryBuilder> { - count(this: T): Promise; - find(this: T, id: number): Promise; - find(this: T, ids: number[]): Promise; +// Helper type to extract the JSON return type from a model instance +type JSONReturnType = I extends ModelInstance + ? ReturnType + : never; + +// Helper type to determine if the query will return a single item or array +type QueryResult = IsSingle extends true ? I : I[]; + +interface QueryBuilder< + M extends BaseModel, + I = InstanceType, + IsSingle extends boolean = false +> { + find(this: T, id: number): QueryBuilder; + find(this: T, ids: number[]): this; where(this: T, field: string, value: any): this; where(this: T, field: string, operator: string, value?: any): this; whereIn(this: T, field: string, values: (string | number)[]): this; with(this: T, relationName: string | string[]): this; limit(this: T, limit: number): this; - get(): Promise; - first(): Promise; orderBy(this: T, field: string, direction?: "asc" | "desc"): this; - then(onfulfilled?: (value: R[]) => R[] | PromiseLike): Promise; - catch(onrejected?: (reason: any) => any): Promise; + // Changes return type to single instance: + first(): QueryBuilder; + + // Promise-returning methods with specific return types + count(this: T): Promise; + get(): Promise>; + toJSON(): Promise< + IsSingle extends true ? JSONReturnType : JSONReturnType[] + >; + + // Promise compatibility + then>( + onfulfilled?: ( + value: QueryResult + ) => Result | PromiseLike + ): Promise; + catch>( + onrejected?: ((reason: any) => Result | PromiseLike) | null + ): Promise; } declare const Model: BaseModel; diff --git a/packages/superflare/src/model.ts b/packages/superflare/src/model.ts index 67f645c5..de94d278 100644 --- a/packages/superflare/src/model.ts +++ b/packages/superflare/src/model.ts @@ -34,7 +34,7 @@ export class Model { return new Proxy(this, { get(target, prop) { - if (prop in target) { + if (target[prop as keyof Model] !== undefined) { return target[prop as keyof Model]; } @@ -46,7 +46,7 @@ export class Model { return target.attributes[prop]; } - // If trying to access a relation property, and it hasn't be set yet, call the relation function. + // If trying to access a relation property that hasn't been set yet, call the relation function. if (typeof prop === "string" && target[`$${prop}` as keyof Model]) { return target[`$${prop}` as keyof Model](); } @@ -59,6 +59,10 @@ export class Model { target.attributes[prop] = value; return true; }, + + ownKeys(target) { + return Object.keys(target.toJSON()); + }, }); } @@ -220,8 +224,8 @@ export class Model { const value = this.relations[key]; acc[key] = value instanceof Array - ? value.map((model) => model.serialize()) - : value.serialize(); + ? value.map((model) => model.toJSON()) + : value.toJSON(); return acc; }, {} as Record); } diff --git a/packages/superflare/src/query-builder.ts b/packages/superflare/src/query-builder.ts index 3f9b78e1..88125916 100644 --- a/packages/superflare/src/query-builder.ts +++ b/packages/superflare/src/query-builder.ts @@ -10,6 +10,7 @@ export class QueryBuilder { private $eagerLoad: string[] = []; private $limit: number | null = null; private $single: boolean = false; + private $serialize: boolean = false; private $modelClass: any; private $afterHooks: ((results: any) => void)[] = []; @@ -69,6 +70,9 @@ export class QueryBuilder { results = await this.eagerLoadRelations(results); + results = this.$serialize + ? results.map((result: typeof this.$modelClass) => result.toJSON()) + : results; let result = this.$single ? results[0] ?? null : results; this.runCallbacks(result); @@ -139,6 +143,11 @@ export class QueryBuilder { return this.limit(1); } + toJSON(): this { + this.$serialize = true; + return this; + } + async insert(attributes: Record): Promise { try { const results = await this.connection() diff --git a/packages/superflare/tests/d1-types.test.ts b/packages/superflare/tests/d1-types.test.ts index 725c37d5..ccee50e6 100644 --- a/packages/superflare/tests/d1-types.test.ts +++ b/packages/superflare/tests/d1-types.test.ts @@ -146,7 +146,7 @@ describe("syncSuperflareTypes", () => { const types = await generateTypesFromSqlite(db); const results = syncSuperflareTypes(tmpDir, modelsDir, types); - // Expect `user.ts` to not exist + // Expect `User.ts` to not exist expect(fs.existsSync(path.join(tmpDir, "User.ts"))).toBe(false); expect(fs.readFileSync(path.join(modelsDir, "Post.ts"), "utf8")).toBe( @@ -173,7 +173,7 @@ describe("syncSuperflareTypes", () => { createIfNotFound: true, }); - // Expect `user.ts` to have been created + // Expect `User.ts` to have been created expect(fs.existsSync(path.join(modelsDir, "User.ts"))).toBe(true); expect(fs.readFileSync(path.join(modelsDir, "User.ts"), "utf8")).toBe( "import { Model } from 'superflare';\n\n" + diff --git a/packages/superflare/tests/model/belongs-to.test.ts b/packages/superflare/tests/model/belongs-to.test.ts index 864992d4..aad56d9a 100644 --- a/packages/superflare/tests/model/belongs-to.test.ts +++ b/packages/superflare/tests/model/belongs-to.test.ts @@ -14,7 +14,7 @@ class Post extends ModelConstructor { updatedAt!: string; userId!: number; - user!: User | Promise; + user!: User; $user() { return this.belongsTo(User); } diff --git a/templates/remix/app/routes/_auth.login.tsx b/templates/remix/app/routes/_auth.login.tsx index c4a9b8b3..1b8161c6 100644 --- a/templates/remix/app/routes/_auth.login.tsx +++ b/templates/remix/app/routes/_auth.login.tsx @@ -1,5 +1,5 @@ import { Form, Link, useActionData } from "@remix-run/react"; -import { json, redirect, type ActionFunctionArgs } from "@remix-run/cloudflare"; +import { redirect, type ActionFunctionArgs } from "@remix-run/cloudflare"; import { User } from "~/models/User"; export async function action({ @@ -18,11 +18,11 @@ export async function action({ return redirect("/dashboard"); } - return json({ error: "Invalid credentials" }, { status: 400 }); + return Response.json({ error: "Invalid credentials" }, { status: 400 }); } export default function Login() { - const actionData = useActionData(); + const actionData = useActionData<{ error: string }>(); return ( diff --git a/templates/remix/app/routes/_auth.register.tsx b/templates/remix/app/routes/_auth.register.tsx index 8955e03c..a17ead1b 100644 --- a/templates/remix/app/routes/_auth.register.tsx +++ b/templates/remix/app/routes/_auth.register.tsx @@ -1,5 +1,5 @@ import { Form, Link, useActionData } from "@remix-run/react"; -import { json, redirect, type ActionFunctionArgs } from "@remix-run/cloudflare"; +import { redirect, type ActionFunctionArgs } from "@remix-run/cloudflare"; import { User } from "~/models/User"; import { hash } from "superflare"; @@ -16,7 +16,7 @@ export async function action({ const password = formData.get("password") as string; if (await User.where("email", email).count()) { - return json({ error: "Email already exists" }, { status: 400 }); + return Response.json({ error: "Email already exists" }, { status: 400 }); } const user = await User.create({ @@ -30,7 +30,7 @@ export async function action({ } export default function Register() { - const actionData = useActionData(); + const actionData = useActionData<{ error: string }>(); return ( diff --git a/templates/remix/app/routes/dashboard.tsx b/templates/remix/app/routes/dashboard.tsx index 24ad1538..0a383815 100644 --- a/templates/remix/app/routes/dashboard.tsx +++ b/templates/remix/app/routes/dashboard.tsx @@ -1,4 +1,4 @@ -import { type LoaderFunctionArgs, redirect, json } from "@remix-run/cloudflare"; +import { type LoaderFunctionArgs, redirect } from "@remix-run/cloudflare"; import { useLoaderData } from "@remix-run/react"; import { User } from "~/models/User"; @@ -7,9 +7,9 @@ export async function loader({ context: { auth } }: LoaderFunctionArgs) { return redirect("/login"); } - return json({ + return { user: (await auth.user(User)) as User, - }); + }; } export default function Dashboard() { diff --git a/templates/remix/package.json b/templates/remix/package.json index 9bb27124..89a9eba4 100644 --- a/templates/remix/package.json +++ b/templates/remix/package.json @@ -7,10 +7,10 @@ "sideEffects": false, "scripts": { "build": "remix vite:build", - "deploy": "wrangler -j deploy", + "deploy": "wrangler deploy", "dev": "superflare dev", - "start": "wrangler -j dev", - "typegen": "wrangler -j types", + "start": "wrangler dev", + "typegen": "wrangler types", "typecheck": "tsc" }, "keywords": [], diff --git a/templates/remix/vite.config.ts b/templates/remix/vite.config.ts index 9d2c63ec..2bcb6055 100644 --- a/templates/remix/vite.config.ts +++ b/templates/remix/vite.config.ts @@ -3,6 +3,13 @@ import { vitePlugin as remix } from "@remix-run/dev"; import { superflareDevProxyVitePlugin } from "@superflare/remix/dev"; import tsconfigPaths from "vite-tsconfig-paths"; +declare module "@remix-run/cloudflare" { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Future { + v3_singleFetch: true; + } +} + export default defineConfig({ plugins: [ superflareDevProxyVitePlugin(), @@ -11,6 +18,7 @@ export default defineConfig({ v3_fetcherPersist: true, v3_lazyRouteDiscovery: true, v3_relativeSplatPath: true, + v3_singleFetch: true, v3_throwAbortReason: true, }, }),