diff --git a/.changeset/config.json b/.changeset/config.json index 2f16bf1..3bcaa07 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,5 @@ "commit": false, "access": "public", "baseBranch": "main", - "ignore": ["pagesdir", "appdir", "custom-filename"] + "ignore": ["pagesdir", "appdir", "custom-filename", "with-config"] } diff --git a/.changeset/witty-experts-juggle.md b/.changeset/witty-experts-juggle.md new file mode 100644 index 0000000..56b8ea1 --- /dev/null +++ b/.changeset/witty-experts-juggle.md @@ -0,0 +1,5 @@ +--- +"next-typesafe-url": minor +--- + +Adds TypeScript configuration file support diff --git a/examples/custom-filename/_next-typesafe-url_.d.ts b/examples/custom-filename/_next-typesafe-url_.d.ts index 8174469..decc9bc 100644 --- a/examples/custom-filename/_next-typesafe-url_.d.ts +++ b/examples/custom-filename/_next-typesafe-url_.d.ts @@ -7,7 +7,7 @@ declare module "@@@next-typesafe-url" { import type { InferRoute, StaticRoute } from "next-typesafe-url"; - + interface DynamicRouter { "/[slug]/[...foo]": InferRoute; } diff --git a/examples/with-config/.gitignore b/examples/with-config/.gitignore new file mode 100644 index 0000000..27ec5cb --- /dev/null +++ b/examples/with-config/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# generated types +_next-typesafe-url_.d.ts diff --git a/examples/with-config/next-typesafe-url.config.ts b/examples/with-config/next-typesafe-url.config.ts new file mode 100644 index 0000000..5a4ddcc --- /dev/null +++ b/examples/with-config/next-typesafe-url.config.ts @@ -0,0 +1,14 @@ +import type { Config } from "next-typesafe-url"; + +const config: Config = { + // Optional: customize the output path + outputPath: "./next_safe_routes.d.ts", + // Optional: customize the src directory + srcPath: "./src", + // Optional: customize page extensions + pageExtensions: ["tsx", "ts", "jsx", "js"], + // Optional: customize the routeType filename + filename: "route-type", +}; + +export default config; diff --git a/examples/with-config/next.config.mjs b/examples/with-config/next.config.mjs new file mode 100644 index 0000000..4678774 --- /dev/null +++ b/examples/with-config/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/examples/with-config/next_safe_routes.d.ts b/examples/with-config/next_safe_routes.d.ts new file mode 100644 index 0000000..052e8a4 --- /dev/null +++ b/examples/with-config/next_safe_routes.d.ts @@ -0,0 +1,19 @@ +// This file is generated by next-typesafe-url +// Do not edit this file directly. + +// @generated +// prettier-ignore +/* eslint-disable */ + +declare module "@@@next-typesafe-url" { + import type { InferRoute, StaticRoute } from "next-typesafe-url"; + + interface DynamicRouter { + "/blog/[slug]": InferRoute; + } + + interface StaticRouter { + "/about": StaticRoute; + "/": StaticRoute; + } +} diff --git a/examples/with-config/package.json b/examples/with-config/package.json new file mode 100644 index 0000000..c25afe1 --- /dev/null +++ b/examples/with-config/package.json @@ -0,0 +1,22 @@ +{ + "name": "with-config", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "concurrently \"next-typesafe-url -w\" \"next dev\"", + "build": "next-typesafe-url && next build", + "start": "next start" + }, + "dependencies": { + "@types/node": "22.0.0", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", + "concurrently": "^8.0.1", + "next": "15.0.0", + "next-typesafe-url": "workspace:*", + "react": "19.0.0", + "react-dom": "19.0.0", + "typescript": "^5", + "zod": "^4.1.12" + } +} diff --git a/examples/with-config/src/app/about/page.tsx b/examples/with-config/src/app/about/page.tsx new file mode 100644 index 0000000..8d35563 --- /dev/null +++ b/examples/with-config/src/app/about/page.tsx @@ -0,0 +1,14 @@ +export default function About() { + return ( +
+

About

+

+ This is a simple static route that doesn't require route parameters. +

+

+ Static routes are automatically typed and available in the router + autocomplete. +

+
+ ); +} diff --git a/examples/with-config/src/app/blog/[slug]/page.tsx b/examples/with-config/src/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..249b1c7 --- /dev/null +++ b/examples/with-config/src/app/blog/[slug]/page.tsx @@ -0,0 +1,22 @@ +import { PageProps, Route } from "./route-type"; +import { withParamValidation } from "next-typesafe-url/app/hoc"; + +async function BlogPost({ routeParams }: PageProps) { + const { slug } = await routeParams; + + return ( +
+

Blog Post: {slug}

+

+ This route demonstrates type-safe dynamic route parameters using + next-typesafe-url. +

+

+ The slug parameter is validated at runtime and fully typed + at compile time. +

+
+ ); +} + +export default withParamValidation(BlogPost, Route); diff --git a/examples/with-config/src/app/blog/[slug]/route-type.ts b/examples/with-config/src/app/blog/[slug]/route-type.ts new file mode 100644 index 0000000..a2ab89a --- /dev/null +++ b/examples/with-config/src/app/blog/[slug]/route-type.ts @@ -0,0 +1,12 @@ +import type { DynamicRoute, InferPagePropsType } from "next-typesafe-url"; + +import { z } from "zod"; + +export const Route = { + routeParams: z.object({ + slug: z.string(), + }), +} satisfies DynamicRoute; + +export type RouteType = typeof Route; +export type PageProps = InferPagePropsType; diff --git a/examples/with-config/src/app/layout.tsx b/examples/with-config/src/app/layout.tsx new file mode 100644 index 0000000..4ebbad1 --- /dev/null +++ b/examples/with-config/src/app/layout.tsx @@ -0,0 +1,26 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Next.js Typesafe URL - Config Example", + description: "Demonstrating config file usage with next-typesafe-url", +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + +
{children}
+ + + ); +} diff --git a/examples/with-config/src/app/page.tsx b/examples/with-config/src/app/page.tsx new file mode 100644 index 0000000..93e1960 --- /dev/null +++ b/examples/with-config/src/app/page.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { $path } from "next-typesafe-url"; +import { useRouter } from "next/navigation"; + +export default function Home() { + const router = useRouter(); + + const handleNavigate = () => { + // Type-safe navigation with route params + const url = $path({ + route: "/blog/[slug]", + routeParams: { + slug: "my-first-post", + }, + }); + router.push(url); + }; + + return ( +
+

Welcome to Next.js Typesafe URL

+

+ This example demonstrates using a config file ( + next-typesafe-url.config.ts) instead of CLI arguments. +

+ +

Features:

+
    +
  • ✅ Type-safe routing
  • +
  • ✅ Configuration file support
  • +
  • ✅ Next.js 15 and React 19
  • +
  • ✅ Automatic type generation
  • +
+ + +
+ ); +} diff --git a/examples/with-config/tsconfig.json b/examples/with-config/tsconfig.json new file mode 100644 index 0000000..c133409 --- /dev/null +++ b/examples/with-config/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/next-typesafe-url/CHANGELOG.md b/packages/next-typesafe-url/CHANGELOG.md index d9a9718..4c56f40 100644 --- a/packages/next-typesafe-url/CHANGELOG.md +++ b/packages/next-typesafe-url/CHANGELOG.md @@ -156,7 +156,6 @@ ### Major Changes - 88cdd83: - major changes - - significant refactor to internal encoding + decoding logic- now simpler and easier to follow - complete documentation rewrite - code generation overhaul diff --git a/packages/next-typesafe-url/package.json b/packages/next-typesafe-url/package.json index ea0b97f..e52c778 100644 --- a/packages/next-typesafe-url/package.json +++ b/packages/next-typesafe-url/package.json @@ -62,6 +62,8 @@ }, "dependencies": { "chokidar": "^3.5.3", + "cosmiconfig": "^9.0.0", + "jiti": "^1.21.0", "meow": "9.0.0" }, "devDependencies": { diff --git a/packages/next-typesafe-url/src/cli.ts b/packages/next-typesafe-url/src/cli.ts index dae3d66..1e4bce0 100644 --- a/packages/next-typesafe-url/src/cli.ts +++ b/packages/next-typesafe-url/src/cli.ts @@ -7,12 +7,18 @@ import { getAPPRoutesWithExportedRoute, generateTypesFile, } from "./generateTypes"; +import { loadConfig } from "./loadConfig"; +import { defaultConfig } from "./config"; const helpText = ` Usage: $ npx next-typesafe-url (...options) Scans for routes in your app and pages directories and generates a types file for next-typesafe-url +Configuration: +You can use a next-typesafe-url.config.ts file for fully typesafe configuration. +CLI options will override config file values. + Options: --watch / -w, Watch for file changes in src/app and src/pages and regenerate the types file --srcPath, The path to your src directory relative to the cwd the cli is run from. DEFAULT: "./src" @@ -30,19 +36,15 @@ const cli = meow(helpText, { }, outputPath: { type: "string", - default: "./_next-typesafe-url_.d.ts", }, srcPath: { type: "string", - default: "./src", }, pageExtensions: { type: "string", - default: "tsx,ts,jsx,js", }, filename: { type: "string", - default: "routeType", }, }, }); @@ -126,41 +128,72 @@ function watch({ } if (require.main === module) { - const { filename, srcPath, outputPath } = cli.flags; - const pageExtensions = cli.flags.pageExtensions.split(","); - - const absoluteSrcPath = path.join(process.cwd(), srcPath); - - if (!directoryExistsSync(absoluteSrcPath)) { - console.log("srcPath is not a directory or does not exist"); - process.exit(1); - } - - const absoluteOutputPath = path.join(process.cwd(), outputPath); - const relativePathFromOutputToSrc = path.relative( - path.dirname(absoluteOutputPath), - absoluteSrcPath, - ); - - const appPath = path.join(absoluteSrcPath, "app"); - const pagesPath = path.join(absoluteSrcPath, "pages"); - - const absoluteAppPath = directoryExistsSync(appPath) ? appPath : null; - const absolutePagesPath = directoryExistsSync(pagesPath) ? pagesPath : null; - - const paths = { - absolutePagesPath, - absoluteAppPath, - absoluteOutputPath, - relativePathFromOutputToSrc, - }; - - if (cli.flags.watch) { - build({ filename, paths, pageExtensions }); - watch({ filename, paths, pageExtensions }); - } else { - build({ filename, paths, pageExtensions }); - } + void (async () => { + // load config from the file + const fileConfig = await loadConfig(); + + // helper function to normalize pageExtensions to an array + const normalizePageExtensions = ( + extensions: string | string[] | undefined, + ): string[] => { + if (!extensions) return defaultConfig.pageExtensions; + if (typeof extensions === "string") { + return extensions.split(",").map((ext) => ext.trim()); + } + return extensions; + }; + + // merge config: CLI flags > config file > defaults + const mergedConfig = { + watch: cli.flags.watch ?? fileConfig?.watch ?? defaultConfig.watch, + srcPath: + cli.flags.srcPath ?? fileConfig?.srcPath ?? defaultConfig.srcPath, + outputPath: + cli.flags.outputPath ?? + fileConfig?.outputPath ?? + defaultConfig.outputPath, + filename: + cli.flags.filename ?? fileConfig?.filename ?? defaultConfig.filename, + pageExtensions: normalizePageExtensions( + cli.flags.pageExtensions ?? fileConfig?.pageExtensions, + ), + }; + + const { filename, srcPath, outputPath, pageExtensions } = mergedConfig; + + const absoluteSrcPath = path.join(process.cwd(), srcPath); + + if (!directoryExistsSync(absoluteSrcPath)) { + console.log("srcPath is not a directory or does not exist"); + process.exit(1); + } + + const absoluteOutputPath = path.join(process.cwd(), outputPath); + const relativePathFromOutputToSrc = path.relative( + path.dirname(absoluteOutputPath), + absoluteSrcPath, + ); + + const appPath = path.join(absoluteSrcPath, "app"); + const pagesPath = path.join(absoluteSrcPath, "pages"); + + const absoluteAppPath = directoryExistsSync(appPath) ? appPath : null; + const absolutePagesPath = directoryExistsSync(pagesPath) ? pagesPath : null; + + const paths = { + absolutePagesPath, + absoluteAppPath, + absoluteOutputPath, + relativePathFromOutputToSrc, + }; + + if (mergedConfig.watch) { + build({ filename, paths, pageExtensions }); + watch({ filename, paths, pageExtensions }); + } else { + build({ filename, paths, pageExtensions }); + } + })(); } import fs from "fs"; diff --git a/packages/next-typesafe-url/src/config.ts b/packages/next-typesafe-url/src/config.ts new file mode 100644 index 0000000..d83cdbf --- /dev/null +++ b/packages/next-typesafe-url/src/config.ts @@ -0,0 +1,57 @@ +/** + * Configuration options for next-typesafe-url CLI + */ +export interface Config { + /** + * Watch for file changes in src/app and src/pages and regenerate the types file + * @default false + */ + watch?: boolean; + + /** + * The path to your src directory relative to the cwd the cli is run from + * @default "./src" + */ + srcPath?: string; + + /** + * The path of the generated .d.ts file relative to the cwd the cli is run from + * @default "./_next-typesafe-url_.d.ts" + */ + outputPath?: string; + + /** + * A list of file extensions to consider as page files + * Can be an array of strings or a comma-separated string + * @default ["tsx", "ts", "jsx", "js"] + */ + pageExtensions?: string[] | string; + + /** + * Override the default name of the RouteType file in the app directory + * @default "routeType" + */ + filename?: string; +} + +/** + * Resolved configuration with all options explicitly set + */ +export interface ResolvedConfig { + watch: boolean; + srcPath: string; + outputPath: string; + pageExtensions: string[]; + filename: string; +} + +/** + * Default configuration values + */ +export const defaultConfig: ResolvedConfig = { + watch: false, + srcPath: "./src", + outputPath: "./_next-typesafe-url_.d.ts", + pageExtensions: ["tsx", "ts", "jsx", "js"], + filename: "routeType", +}; diff --git a/packages/next-typesafe-url/src/index.ts b/packages/next-typesafe-url/src/index.ts index 1e7b64e..8ac07e9 100644 --- a/packages/next-typesafe-url/src/index.ts +++ b/packages/next-typesafe-url/src/index.ts @@ -14,6 +14,7 @@ import type { UseParamsResult, ServerParseParamsResult, } from "./types"; +import type { Config } from "./config"; export type { AllRoutes, @@ -28,6 +29,7 @@ export type { StaticRoute, UseParamsResult, ServerParseParamsResult, + Config, }; // * TESTED diff --git a/packages/next-typesafe-url/src/loadConfig.ts b/packages/next-typesafe-url/src/loadConfig.ts new file mode 100644 index 0000000..8b966f9 --- /dev/null +++ b/packages/next-typesafe-url/src/loadConfig.ts @@ -0,0 +1,82 @@ +import { cosmiconfig } from "cosmiconfig"; +import createJiti from "jiti"; +import path from "path"; +import type { Config } from "./config"; + +const MODULE_NAME = "next-typesafe-url"; + +/** + * Loads the next-typesafe-url configuration from a config file + * Searches for configuration files in this order: + * - next-typesafe-url.config.ts + * - next-typesafe-url.config.js + * - next-typesafe-url.config.mjs + * - next-typesafe-url.config.cjs + * - package.json (under "next-typesafe-url" key) + * + * @param searchFrom - Directory to start searching from (defaults to process.cwd()) + * @returns The loaded configuration or null if no config file found + */ +export async function loadConfig(searchFrom?: string): Promise { + const explorer = cosmiconfig(MODULE_NAME, { + searchPlaces: [ + `${MODULE_NAME}.config.ts`, + `${MODULE_NAME}.config.js`, + `${MODULE_NAME}.config.mjs`, + `${MODULE_NAME}.config.cjs`, + "package.json", + ], + loaders: { + ".ts": createTypeScriptLoader(), + }, + }); + + try { + const result = await explorer.search(searchFrom); + + if (!result || result.isEmpty) { + return null; + } + + // validate that the config is an object + if (typeof result.config !== "object" || result.config === null) { + throw new Error( + `Invalid config file at ${result.filepath}: Expected an object, got ${typeof result.config}`, + ); + } + + return result.config as Config; + } catch (error) { + if (error instanceof Error) { + console.error(`Error loading config: ${error.message}`); + process.exit(1); + } + throw error; + } +} + +/** + * Creates a loader for TypeScript config files using jiti + */ +function createTypeScriptLoader() { + return (filepath: string) => { + // create a jiti instance with the directory of the config file as the base + const jiti = createJiti(path.dirname(filepath), { + interopDefault: true, + esmResolve: true, + }); + + try { + // load the config file by its basename + const loaded = jiti(`./${path.basename(filepath)}`) as { + default?: unknown; + }; + // handle both default exports and named exports + return loaded.default || loaded; + } catch (error) { + throw new Error( + `Failed to load TypeScript config from ${filepath}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }; +} diff --git a/packages/next-typesafe-url/test/configMerging.test.ts b/packages/next-typesafe-url/test/configMerging.test.ts new file mode 100644 index 0000000..79e32f2 --- /dev/null +++ b/packages/next-typesafe-url/test/configMerging.test.ts @@ -0,0 +1,358 @@ +import { describe, expect, test } from "vitest"; +import { defaultConfig } from "../src/config"; +import type { Config } from "../src/config"; + +/** + * These tests verify the config merging logic used in cli.ts + * The merging precedence is: CLI flags > Config file > Defaults + */ + +// helper function that mimics the CLI merging logic +function mergeConfig( + cliFlags: Partial<{ + watch: boolean; + srcPath: string; + outputPath: string; + pageExtensions: string; + filename: string; + }>, + fileConfig: Config | null, +) { + const normalizePageExtensions = ( + extensions: string | string[] | undefined, + ): string[] => { + if (!extensions) return defaultConfig.pageExtensions; + if (typeof extensions === "string") { + return extensions.split(",").map((ext) => ext.trim()); + } + return extensions; + }; + + return { + watch: cliFlags.watch ?? fileConfig?.watch ?? defaultConfig.watch, + srcPath: cliFlags.srcPath ?? fileConfig?.srcPath ?? defaultConfig.srcPath, + outputPath: + cliFlags.outputPath ?? fileConfig?.outputPath ?? defaultConfig.outputPath, + filename: + cliFlags.filename ?? fileConfig?.filename ?? defaultConfig.filename, + pageExtensions: normalizePageExtensions( + cliFlags.pageExtensions ?? fileConfig?.pageExtensions, + ), + }; +} + +describe("config merging", () => { + describe("precedence: CLI > config file > defaults", () => { + test("uses defaults when no CLI flags or config file", () => { + const merged = mergeConfig({}, null); + + expect(merged).toEqual({ + watch: false, + srcPath: "./src", + outputPath: "./_next-typesafe-url_.d.ts", + filename: "routeType", + pageExtensions: ["tsx", "ts", "jsx", "js"], + }); + }); + + test("config file overrides defaults", () => { + const merged = mergeConfig( + {}, + { + filename: "route-type", + srcPath: "./app", + }, + ); + + expect(merged).toEqual({ + watch: false, + srcPath: "./app", + outputPath: "./_next-typesafe-url_.d.ts", + filename: "route-type", + pageExtensions: ["tsx", "ts", "jsx", "js"], + }); + }); + + test("CLI flags override config file", () => { + const merged = mergeConfig( + { + filename: "cli-route", + }, + { + filename: "config-route", + srcPath: "./app", + }, + ); + + expect(merged).toEqual({ + watch: false, + srcPath: "./app", // from config + outputPath: "./_next-typesafe-url_.d.ts", + filename: "cli-route", // from CLI (overrides config) + pageExtensions: ["tsx", "ts", "jsx", "js"], + }); + }); + + test("CLI flags override defaults directly", () => { + const merged = mergeConfig( + { + filename: "cli-route", + srcPath: "./custom", + }, + null, + ); + + expect(merged).toEqual({ + watch: false, + srcPath: "./custom", + outputPath: "./_next-typesafe-url_.d.ts", + filename: "cli-route", + pageExtensions: ["tsx", "ts", "jsx", "js"], + }); + }); + + test("all three sources combined correctly", () => { + const merged = mergeConfig( + { + watch: true, // CLI + }, + { + filename: "config-route", // config + outputPath: "./types.d.ts", // config + }, + // srcPath and pageExtensions will be defaults + ); + + expect(merged).toEqual({ + watch: true, // from CLI + srcPath: "./src", // from defaults + outputPath: "./types.d.ts", // from config + filename: "config-route", // from config + pageExtensions: ["tsx", "ts", "jsx", "js"], // from defaults + }); + }); + }); + + describe("pageExtensions normalization", () => { + test("converts string to array", () => { + const merged = mergeConfig( + { + pageExtensions: "mdx,tsx,ts", + }, + null, + ); + + expect(merged.pageExtensions).toEqual(["mdx", "tsx", "ts"]); + }); + + test("trims whitespace from string", () => { + const merged = mergeConfig( + { + pageExtensions: "mdx, tsx, ts ", + }, + null, + ); + + expect(merged.pageExtensions).toEqual(["mdx", "tsx", "ts"]); + }); + + test("keeps array as array from config", () => { + const merged = mergeConfig( + {}, + { + pageExtensions: ["mdx", "tsx"], + }, + ); + + expect(merged.pageExtensions).toEqual(["mdx", "tsx"]); + }); + + test("CLI string overrides config array", () => { + const merged = mergeConfig( + { + pageExtensions: "js,jsx", + }, + { + pageExtensions: ["mdx", "tsx"], + }, + ); + + expect(merged.pageExtensions).toEqual(["js", "jsx"]); + }); + + test("uses defaults when undefined", () => { + const merged = mergeConfig({}, {}); + + expect(merged.pageExtensions).toEqual(["tsx", "ts", "jsx", "js"]); + }); + }); + + describe("boolean flags", () => { + test("watch flag from CLI", () => { + const merged = mergeConfig( + { + watch: true, + }, + null, + ); + + expect(merged.watch).toBe(true); + }); + + test("watch flag from config", () => { + const merged = mergeConfig( + {}, + { + watch: true, + }, + ); + + expect(merged.watch).toBe(true); + }); + + test("watch defaults to false", () => { + const merged = mergeConfig({}, null); + + expect(merged.watch).toBe(false); + }); + + test("CLI watch overrides config watch", () => { + const merged = mergeConfig( + { + watch: true, + }, + { + watch: false, + }, + ); + + expect(merged.watch).toBe(true); + }); + }); + + describe("individual options", () => { + test("srcPath merging", () => { + expect(mergeConfig({}, null).srcPath).toBe("./src"); + expect(mergeConfig({}, { srcPath: "./app" }).srcPath).toBe("./app"); + expect(mergeConfig({ srcPath: "./cli-src" }, null).srcPath).toBe( + "./cli-src", + ); + expect( + mergeConfig({ srcPath: "./cli-src" }, { srcPath: "./config-src" }) + .srcPath, + ).toBe("./cli-src"); + }); + + test("outputPath merging", () => { + expect(mergeConfig({}, null).outputPath).toBe( + "./_next-typesafe-url_.d.ts", + ); + expect(mergeConfig({}, { outputPath: "./types.d.ts" }).outputPath).toBe( + "./types.d.ts", + ); + expect(mergeConfig({ outputPath: "./cli.d.ts" }, null).outputPath).toBe( + "./cli.d.ts", + ); + expect( + mergeConfig( + { outputPath: "./cli.d.ts" }, + { outputPath: "./config.d.ts" }, + ).outputPath, + ).toBe("./cli.d.ts"); + }); + + test("filename merging", () => { + expect(mergeConfig({}, null).filename).toBe("routeType"); + expect(mergeConfig({}, { filename: "route" }).filename).toBe("route"); + expect(mergeConfig({ filename: "cli-route" }, null).filename).toBe( + "cli-route", + ); + expect( + mergeConfig({ filename: "cli-route" }, { filename: "config-route" }) + .filename, + ).toBe("cli-route"); + }); + }); + + describe("edge cases", () => { + test("handles empty config object", () => { + const merged = mergeConfig({}, {}); + + expect(merged).toEqual({ + watch: false, + srcPath: "./src", + outputPath: "./_next-typesafe-url_.d.ts", + filename: "routeType", + pageExtensions: ["tsx", "ts", "jsx", "js"], + }); + }); + + test("handles full config with all options", () => { + const merged = mergeConfig( + {}, + { + watch: true, + srcPath: "./custom-src", + outputPath: "./custom-output.d.ts", + pageExtensions: ["mdx", "tsx"], + filename: "custom-route", + }, + ); + + expect(merged).toEqual({ + watch: true, + srcPath: "./custom-src", + outputPath: "./custom-output.d.ts", + filename: "custom-route", + pageExtensions: ["mdx", "tsx"], + }); + }); + + test("handles CLI overriding all config options", () => { + const merged = mergeConfig( + { + watch: false, + srcPath: "./cli-src", + outputPath: "./cli-output.d.ts", + pageExtensions: "js,jsx", + filename: "cli-route", + }, + { + watch: true, + srcPath: "./config-src", + outputPath: "./config-output.d.ts", + pageExtensions: ["tsx", "ts"], + filename: "config-route", + }, + ); + + expect(merged).toEqual({ + watch: false, + srcPath: "./cli-src", + outputPath: "./cli-output.d.ts", + filename: "cli-route", + pageExtensions: ["js", "jsx"], + }); + }); + + test("handles partial CLI flags with partial config", () => { + const merged = mergeConfig( + { + filename: "cli-filename", + }, + { + srcPath: "./config-src", + watch: true, + }, + ); + + expect(merged).toEqual({ + watch: true, // from config + srcPath: "./config-src", // from config + outputPath: "./_next-typesafe-url_.d.ts", // from defaults + filename: "cli-filename", // from CLI + pageExtensions: ["tsx", "ts", "jsx", "js"], // from defaults + }); + }); + }); +}); diff --git a/packages/next-typesafe-url/test/loadConfig.test.ts b/packages/next-typesafe-url/test/loadConfig.test.ts new file mode 100644 index 0000000..ab998f4 --- /dev/null +++ b/packages/next-typesafe-url/test/loadConfig.test.ts @@ -0,0 +1,373 @@ +import { describe, expect, test, beforeEach, afterEach, vi } from "vitest"; +import { loadConfig } from "../src/loadConfig"; +import fs from "fs"; +import path from "path"; +import os from "os"; + +describe("loadConfig", () => { + let tempDir: string; + + beforeEach(() => { + // create a temporary directory for test files + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "next-typesafe-url-test-")); + }); + + afterEach(() => { + // clean up temporary directory + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + describe("config file discovery", () => { + test("loads .ts config file", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.ts"); + fs.writeFileSync( + configPath, + ` + export default { + filename: "route-type", + srcPath: "./custom-src", + }; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config).toEqual({ + filename: "route-type", + srcPath: "./custom-src", + }); + }); + + test("loads .js config file", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.js"); + fs.writeFileSync( + configPath, + ` + module.exports = { + filename: "route", + outputPath: "./types.d.ts", + }; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config).toEqual({ + filename: "route", + outputPath: "./types.d.ts", + }); + }); + + test("loads .mjs config file", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.mjs"); + fs.writeFileSync( + configPath, + ` + export default { + pageExtensions: ["tsx", "ts"], + }; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config).toEqual({ + pageExtensions: ["tsx", "ts"], + }); + }); + + test("loads .cjs config file", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.cjs"); + fs.writeFileSync( + configPath, + ` + module.exports = { + watch: true, + }; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config).toEqual({ + watch: true, + }); + }); + + test("loads from package.json", async () => { + const packagePath = path.join(tempDir, "package.json"); + fs.writeFileSync( + packagePath, + JSON.stringify({ + name: "test", + "next-typesafe-url": { + filename: "custom-route", + srcPath: "./app", + }, + }), + ); + + const config = await loadConfig(tempDir); + + expect(config).toEqual({ + filename: "custom-route", + srcPath: "./app", + }); + }); + + test("returns null when no config file exists", async () => { + const config = await loadConfig(tempDir); + expect(config).toBeNull(); + }); + + test("prioritizes .ts over .js", async () => { + // Create both .ts and .js files + fs.writeFileSync( + path.join(tempDir, "next-typesafe-url.config.ts"), + `export default { filename: "from-ts" };`, + ); + fs.writeFileSync( + path.join(tempDir, "next-typesafe-url.config.js"), + `module.exports = { filename: "from-js" };`, + ); + + const config = await loadConfig(tempDir); + + // Should load .ts first + expect(config).toEqual({ filename: "from-ts" }); + }); + }); + + describe("config parsing", () => { + test("parses valid full config", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.ts"); + fs.writeFileSync( + configPath, + ` + export default { + watch: true, + srcPath: "./src", + outputPath: "./types.d.ts", + pageExtensions: ["tsx", "ts", "jsx", "js"], + filename: "route-type", + }; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config).toEqual({ + watch: true, + srcPath: "./src", + outputPath: "./types.d.ts", + pageExtensions: ["tsx", "ts", "jsx", "js"], + filename: "route-type", + }); + }); + + test("parses partial config", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.ts"); + fs.writeFileSync( + configPath, + ` + export default { + filename: "route", + }; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config).toEqual({ + filename: "route", + }); + }); + + test("parses empty config object", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.ts"); + fs.writeFileSync( + configPath, + ` + export default {}; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config).toEqual({}); + }); + + test("handles pageExtensions as array", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.ts"); + fs.writeFileSync( + configPath, + ` + export default { + pageExtensions: ["mdx", "tsx"], + }; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config?.pageExtensions).toEqual(["mdx", "tsx"]); + }); + + test("handles pageExtensions as string", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.ts"); + fs.writeFileSync( + configPath, + ` + export default { + pageExtensions: "mdx,tsx", + }; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config?.pageExtensions).toBe("mdx,tsx"); + }); + }); + + describe("TypeScript config with imports", () => { + test("loads config that imports types", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.ts"); + fs.writeFileSync( + configPath, + ` + import type { Config } from "../src/config"; + + const config: Config = { + filename: "typed-route", + }; + + export default config; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config).toEqual({ + filename: "typed-route", + }); + }); + }); + + describe("error handling", () => { + test("exits process for invalid config (non-object)", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.ts"); + fs.writeFileSync( + configPath, + ` + export default "invalid"; + `, + ); + + const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit called"); + }); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + await expect(loadConfig(tempDir)).rejects.toThrow("process.exit called"); + + expect(exitSpy).toHaveBeenCalledWith(1); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining("Invalid config file"), + ); + + exitSpy.mockRestore(); + errorSpy.mockRestore(); + }); + + test("exits process for syntax errors in TypeScript config", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.ts"); + fs.writeFileSync( + configPath, + ` + export default { + filename: "test" + missing: "comma" + }; + `, + ); + + const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit called"); + }); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + await expect(loadConfig(tempDir)).rejects.toThrow("process.exit called"); + + expect(exitSpy).toHaveBeenCalledWith(1); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining("Error loading config"), + ); + + exitSpy.mockRestore(); + errorSpy.mockRestore(); + }); + }); + + describe("export styles", () => { + test("handles default export", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.ts"); + fs.writeFileSync( + configPath, + ` + export default { + filename: "default-export", + }; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config).toEqual({ + filename: "default-export", + }); + }); + + test("handles CommonJS module.exports", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.js"); + fs.writeFileSync( + configPath, + ` + module.exports = { + filename: "commonjs-export", + }; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config).toEqual({ + filename: "commonjs-export", + }); + }); + + test("handles const with satisfies", async () => { + const configPath = path.join(tempDir, "next-typesafe-url.config.ts"); + fs.writeFileSync( + configPath, + ` + import type { Config } from "../src/config"; + + const config = { + filename: "satisfies-export", + } satisfies Config; + + export default config; + `, + ); + + const config = await loadConfig(tempDir); + + expect(config).toEqual({ + filename: "satisfies-export", + }); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5dd4aaa..cd97c35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: '@typescript-eslint/eslint-plugin': 5.62.0_575p57tkdo2rnomrmwebxr67lm '@typescript-eslint/parser': 5.62.0_6vi2gkvwlfwgplg2hmgmdmjnte eslint: 8.39.0 - prettier: 3.4.2 - turbo: 2.3.3 + prettier: 3.6.2 + turbo: 2.5.8 typescript: 5.1.3 vitest: 0.32.4 devDependencies: @@ -151,12 +151,38 @@ importers: typescript: 5.0.4 zod: 4.1.12 + examples/with-config: + specifiers: + '@types/node': 22.0.0 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + concurrently: ^8.0.1 + next: 15.0.0 + next-typesafe-url: workspace:* + react: 19.0.0 + react-dom: 19.0.0 + typescript: ^5 + zod: ^4.1.12 + dependencies: + '@types/node': 22.0.0 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + concurrently: 8.2.2 + next: 15.0.0_upmzqgzhjrquwzo2dphtigtdgi + next-typesafe-url: link:../../packages/next-typesafe-url + react: 19.0.0 + react-dom: 19.0.0_react@19.0.0 + typescript: 5.1.3 + zod: 4.1.12 + packages/next-typesafe-url: specifiers: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 chokidar: ^3.5.3 + cosmiconfig: ^9.0.0 eslint: 8.45.0 + jiti: ^1.21.0 meow: 9.0.0 next: 13.4.12 react: 18.2.0 @@ -166,6 +192,8 @@ importers: zod: 4.1.12 dependencies: chokidar: 3.6.0 + cosmiconfig: 9.0.0_typescript@5.1.3 + jiti: 1.21.6 meow: 9.0.0 devDependencies: '@types/react': 18.3.12 @@ -1781,6 +1809,10 @@ packages: /@next/env/13.4.12: resolution: {integrity: sha512-RmHanbV21saP/6OEPBJ7yJMuys68cIf8OBBWd7+uj40LdpmswVAwe1uzeuFyUsd6SfeITWT3XnQfn6wULeKwDQ==} + /@next/env/15.0.0: + resolution: {integrity: sha512-Mcv8ZVmEgTO3bePiH/eJ7zHqQEs2gCqZ0UId2RxHmDDc7Pw6ngfSrOFlxG8XDpaex+n2G+TKPsQAf28MO+88Gw==} + dev: false + /@next/env/15.0.3-canary.1: resolution: {integrity: sha512-J1A96XoPn7rRrElWW6dkiAyb4HVDQ7MB/evf4opEHeYPOo7JX2S5KpG1zjvU0VOJIaOX2yQTQzdbYEsXIQYWLA==} dev: false @@ -1802,8 +1834,8 @@ packages: '@mdx-js/react': optional: true dependencies: - '@mdx-js/loader': 3.1.0_webpack@5.95.0 - '@mdx-js/react': 3.1.0_ys4gee5prganpqdv5owctblfm4 + '@mdx-js/loader': 3.1.0 + '@mdx-js/react': 3.1.0_3pi6y6cylzraaxxtldk4o6b3zi source-map: 0.7.4 dev: false @@ -1824,6 +1856,15 @@ packages: requiresBuild: true optional: true + /@next/swc-darwin-arm64/15.0.0: + resolution: {integrity: sha512-Gjgs3N7cFa40a9QT9AEHnuGKq69/bvIOn0SLGDV+ordq07QOP4k1GDOVedMHEjVeqy1HBLkL8rXnNTuMZIv79A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@next/swc-darwin-arm64/15.0.3-canary.1: resolution: {integrity: sha512-D3WJNj+5e7EG1pZltXYSrcJ77QiZQr0sPmy/igOdCv3j25RY37VsVf48GxtUpO6hQrLSqx5Kf+LggOeVG6lqUw==} engines: {node: '>= 10'} @@ -1850,6 +1891,15 @@ packages: requiresBuild: true optional: true + /@next/swc-darwin-x64/15.0.0: + resolution: {integrity: sha512-BUtTvY5u9s5berAuOEydAUlVMjnl6ZjXS+xVrMt317mglYZ2XXjY8YRDCaz9vYMjBNPXH8Gh75Cew5CMdVbWTw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@next/swc-darwin-x64/15.0.3-canary.1: resolution: {integrity: sha512-Le9Hiev7gOVO8SllnmKis9jZtZ4v/2LV5xx9uh4HoDiGhHYy/aggPE6pfBJCF9GbuxBrFwpUuyx9Hlp80G75Xw==} engines: {node: '>= 10'} @@ -1876,6 +1926,15 @@ packages: requiresBuild: true optional: true + /@next/swc-linux-arm64-gnu/15.0.0: + resolution: {integrity: sha512-sbCoEpuWUBpYoLSgYrk0CkBv8RFv4ZlPxbwqRHr/BWDBJppTBtF53EvsntlfzQJ9fosYX12xnS6ltxYYwsMBjg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@next/swc-linux-arm64-gnu/15.0.3-canary.1: resolution: {integrity: sha512-ZcIDTgCUEwzV84qbp/oYXTZ1PmWygchveCx6bScpMnhpK02NpotOkS3Iko05vNmnr6CO3G7yvYFBwQ0V2u81/Q==} engines: {node: '>= 10'} @@ -1902,6 +1961,15 @@ packages: requiresBuild: true optional: true + /@next/swc-linux-arm64-musl/15.0.0: + resolution: {integrity: sha512-JAw84qfL81aQCirXKP4VkgmhiDpXJupGjt8ITUkHrOVlBd+3h5kjfPva5M0tH2F9KKSgJQHEo3F5S5tDH9h2ww==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@next/swc-linux-arm64-musl/15.0.3-canary.1: resolution: {integrity: sha512-sbCvxvfKAf8ErZ7XfdDxORdrbfKc+94rCV/+N7eQigJdhGu77/F6SnetOCdXfm02AtXIMjLP3vX9DmdXTsr7mA==} engines: {node: '>= 10'} @@ -1928,6 +1996,15 @@ packages: requiresBuild: true optional: true + /@next/swc-linux-x64-gnu/15.0.0: + resolution: {integrity: sha512-r5Smd03PfxrGKMewdRf2RVNA1CU5l2rRlvZLQYZSv7FUsXD5bKEcOZ/6/98aqRwL7diXOwD8TCWJk1NbhATQHg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@next/swc-linux-x64-gnu/15.0.3-canary.1: resolution: {integrity: sha512-CnKUf9YUDrTb2kqyoR5ar43bZAKdmgivsTfsQdRkYw+i0GlbujonFtp45vIctPiRRIkUtwarw11cy4B7fOmvmg==} engines: {node: '>= 10'} @@ -1954,6 +2031,15 @@ packages: requiresBuild: true optional: true + /@next/swc-linux-x64-musl/15.0.0: + resolution: {integrity: sha512-fM6qocafz4Xjhh79CuoQNeGPhDHGBBUbdVtgNFJOUM8Ih5ZpaDZlTvqvqsh5IoO06CGomxurEGqGz/4eR/FaMQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@next/swc-linux-x64-musl/15.0.3-canary.1: resolution: {integrity: sha512-jJOIgoqdqJfjcg23pAacZY13nYrqL2xkpUjPb9dQVVcWnxNzF3qnM1+CgIVNIrWTLVSf7chKxmOpTfLHopR9Dg==} engines: {node: '>= 10'} @@ -1980,6 +2066,15 @@ packages: requiresBuild: true optional: true + /@next/swc-win32-arm64-msvc/15.0.0: + resolution: {integrity: sha512-ZOd7c/Lz1lv7qP/KzR513XEa7QzW5/P0AH3A5eR1+Z/KmDOvMucht0AozccPc0TqhdV1xaXmC0Fdx0hoNzk6ng==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@next/swc-win32-arm64-msvc/15.0.3-canary.1: resolution: {integrity: sha512-jPxgtFz82sFlYotQpDxBPMAE8JQsg0Mx4tTRqdyB177KJwvWEd1GHMnGUb7j1Vx7SJRtepfA6xL+VYL2qpgmjw==} engines: {node: '>= 10'} @@ -2023,6 +2118,15 @@ packages: requiresBuild: true optional: true + /@next/swc-win32-x64-msvc/15.0.0: + resolution: {integrity: sha512-2RVWcLtsqg4LtaoJ3j7RoKpnWHgcrz5XvuUGE7vBYU2i6M2XeD9Y8RlLaF770LEIScrrl8MdWsp6odtC6sZccg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@next/swc-win32-x64-msvc/15.0.3-canary.1: resolution: {integrity: sha512-x9JaB7GlK+cw8MipPR/U1v03jguZCYpwWoPNmspdKgvyKa8Zzn1CbvAljxf71LVw6sYG37ae67WY1zYULSehMw==} engines: {node: '>= 10'} @@ -2365,6 +2469,12 @@ packages: resolution: {integrity: sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==} dev: false + /@types/node/22.0.0: + resolution: {integrity: sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==} + dependencies: + undici-types: 6.11.1 + dev: false + /@types/normalize-package-data/2.4.4: resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} dev: false @@ -3100,7 +3210,7 @@ packages: vitefu: 0.2.5_vite@4.5.5 which-pm: 2.2.0 yargs-parser: 21.1.1 - zod: 3.21.4 + zod: 3.23.8 transitivePeerDependencies: - '@types/node' - less @@ -3540,6 +3650,22 @@ packages: engines: {node: '>= 0.6'} dev: false + /cosmiconfig/9.0.0_typescript@5.1.3: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + typescript: 5.1.3 + dev: false + /cross-spawn/5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -3826,6 +3952,11 @@ packages: strip-ansi: 6.0.1 dev: false + /env-paths/2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + dev: false + /error-ex/1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -4160,7 +4291,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.62.0_6vi2gkvwlfwgplg2hmgmdmjnte + '@typescript-eslint/parser': 5.62.0_iacogk7kkaymxepzhgcbytyi7q debug: 3.2.7 eslint: 8.39.0 eslint-import-resolver-node: 0.3.9 @@ -4180,7 +4311,7 @@ packages: optional: true dependencies: '@rtsao/scc': 1.1.0 - '@typescript-eslint/parser': 5.62.0_6vi2gkvwlfwgplg2hmgmdmjnte + '@typescript-eslint/parser': 5.62.0_iacogk7kkaymxepzhgcbytyi7q array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -6781,6 +6912,51 @@ packages: - '@babel/core' - babel-plugin-macros + /next/15.0.0_upmzqgzhjrquwzo2dphtigtdgi: + resolution: {integrity: sha512-/ivqF6gCShXpKwY9hfrIQYh8YMge8L3W+w1oRLv/POmK4MOQnh+FscZ8a0fRFTSQWE+2z9ctNYvELD9vP2FV+A==} + engines: {node: '>=18.18.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-65a56d0e-20241020 + react-dom: ^18.2.0 || 19.0.0-rc-65a56d0e-20241020 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + dependencies: + '@next/env': 15.0.0 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.13 + busboy: 1.6.0 + caniuse-lite: 1.0.30001674 + postcss: 8.4.31 + react: 19.0.0 + react-dom: 19.0.0_react@19.0.0 + styled-jsx: 5.1.6_react@19.0.0 + optionalDependencies: + '@next/swc-darwin-arm64': 15.0.0 + '@next/swc-darwin-x64': 15.0.0 + '@next/swc-linux-arm64-gnu': 15.0.0 + '@next/swc-linux-arm64-musl': 15.0.0 + '@next/swc-linux-x64-gnu': 15.0.0 + '@next/swc-linux-x64-musl': 15.0.0 + '@next/swc-win32-arm64-msvc': 15.0.0 + '@next/swc-win32-x64-msvc': 15.0.0 + sharp: 0.33.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + /next/15.0.3-canary.1_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-R4k/Bm58HrBlXFuXfuvo7sFk4f0TfRdqpzmO0oJeqJUrYP7KO/f8RFYihq0hu8OD+ePvNx+RzsxOfzxCbmX5LQ==} engines: {node: '>=18.18.0'} @@ -7418,8 +7594,8 @@ packages: hasBin: true dev: false - /prettier/3.4.2: - resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + /prettier/3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} hasBin: true dev: false @@ -7536,6 +7712,15 @@ packages: react: 18.2.0 scheduler: 0.23.2 + /react-dom/19.0.0_react@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} + peerDependencies: + react: ^19.0.0 + dependencies: + react: 19.0.0 + scheduler: 0.25.0 + dev: false + /react-is/16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: false @@ -7550,6 +7735,11 @@ packages: dependencies: loose-envify: 1.4.0 + /react/19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} + engines: {node: '>=0.10.0'} + dev: false + /read-cache/1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} dependencies: @@ -7966,6 +8156,10 @@ packages: dependencies: loose-envify: 1.4.0 + /scheduler/0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + dev: false + /schema-utils/3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} @@ -8432,6 +8626,23 @@ packages: react: 18.2.0 dev: false + /styled-jsx/5.1.6_react@19.0.0: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + client-only: 0.0.1 + react: 19.0.0 + dev: false + /sucrase/3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -8728,64 +8939,64 @@ packages: typescript: 5.1.3 dev: false - /turbo-darwin-64/2.3.3: - resolution: {integrity: sha512-bxX82xe6du/3rPmm4aCC5RdEilIN99VUld4HkFQuw+mvFg6darNBuQxyWSHZTtc25XgYjQrjsV05888w1grpaA==} + /turbo-darwin-64/2.5.8: + resolution: {integrity: sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ==} cpu: [x64] os: [darwin] requiresBuild: true dev: false optional: true - /turbo-darwin-arm64/2.3.3: - resolution: {integrity: sha512-DYbQwa3NsAuWkCUYVzfOUBbSUBVQzH5HWUFy2Kgi3fGjIWVZOFk86ss+xsWu//rlEAfYwEmopigsPYSmW4X15A==} + /turbo-darwin-arm64/2.5.8: + resolution: {integrity: sha512-f1H/tQC9px7+hmXn6Kx/w8Jd/FneIUnvLlcI/7RGHunxfOkKJKvsoiNzySkoHQ8uq1pJnhJ0xNGTlYM48ZaJOQ==} cpu: [arm64] os: [darwin] requiresBuild: true dev: false optional: true - /turbo-linux-64/2.3.3: - resolution: {integrity: sha512-eHj9OIB0dFaP6BxB88jSuaCLsOQSYWBgmhy2ErCu6D2GG6xW3b6e2UWHl/1Ho9FsTg4uVgo4DB9wGsKa5erjUA==} + /turbo-linux-64/2.5.8: + resolution: {integrity: sha512-hMyvc7w7yadBlZBGl/bnR6O+dJTx3XkTeyTTH4zEjERO6ChEs0SrN8jTFj1lueNXKIHh1SnALmy6VctKMGnWfw==} cpu: [x64] os: [linux] requiresBuild: true dev: false optional: true - /turbo-linux-arm64/2.3.3: - resolution: {integrity: sha512-NmDE/NjZoDj1UWBhMtOPmqFLEBKhzGS61KObfrDEbXvU3lekwHeoPvAMfcovzswzch+kN2DrtbNIlz+/rp8OCg==} + /turbo-linux-arm64/2.5.8: + resolution: {integrity: sha512-LQELGa7bAqV2f+3rTMRPnj5G/OHAe2U+0N9BwsZvfMvHSUbsQ3bBMWdSQaYNicok7wOZcHjz2TkESn1hYK6xIQ==} cpu: [arm64] os: [linux] requiresBuild: true dev: false optional: true - /turbo-windows-64/2.3.3: - resolution: {integrity: sha512-O2+BS4QqjK3dOERscXqv7N2GXNcqHr9hXumkMxDj/oGx9oCatIwnnwx34UmzodloSnJpgSqjl8iRWiY65SmYoQ==} + /turbo-windows-64/2.5.8: + resolution: {integrity: sha512-3YdcaW34TrN1AWwqgYL9gUqmZsMT4T7g8Y5Azz+uwwEJW+4sgcJkIi9pYFyU4ZBSjBvkfuPZkGgfStir5BBDJQ==} cpu: [x64] os: [win32] requiresBuild: true dev: false optional: true - /turbo-windows-arm64/2.3.3: - resolution: {integrity: sha512-dW4ZK1r6XLPNYLIKjC4o87HxYidtRRcBeo/hZ9Wng2XM/MqqYkAyzJXJGgRMsc0MMEN9z4+ZIfnSNBrA0b08ag==} + /turbo-windows-arm64/2.5.8: + resolution: {integrity: sha512-eFC5XzLmgXJfnAK3UMTmVECCwuBcORrWdewoiXBnUm934DY6QN8YowC/srhNnROMpaKaqNeRpoB5FxCww3eteQ==} cpu: [arm64] os: [win32] requiresBuild: true dev: false optional: true - /turbo/2.3.3: - resolution: {integrity: sha512-DUHWQAcC8BTiUZDRzAYGvpSpGLiaOQPfYXlCieQbwUvmml/LRGIe3raKdrOPOoiX0DYlzxs2nH6BoWJoZrj8hA==} + /turbo/2.5.8: + resolution: {integrity: sha512-5c9Fdsr9qfpT3hA0EyYSFRZj1dVVsb6KIWubA9JBYZ/9ZEAijgUEae0BBR/Xl/wekt4w65/lYLTFaP3JmwSO8w==} hasBin: true optionalDependencies: - turbo-darwin-64: 2.3.3 - turbo-darwin-arm64: 2.3.3 - turbo-linux-64: 2.3.3 - turbo-linux-arm64: 2.3.3 - turbo-windows-64: 2.3.3 - turbo-windows-arm64: 2.3.3 + turbo-darwin-64: 2.5.8 + turbo-darwin-arm64: 2.5.8 + turbo-linux-64: 2.5.8 + turbo-linux-arm64: 2.5.8 + turbo-windows-64: 2.5.8 + turbo-windows-arm64: 2.5.8 dev: false /type-check/0.4.0: @@ -8898,6 +9109,10 @@ packages: which-boxed-primitive: 1.0.2 dev: false + /undici-types/6.11.1: + resolution: {integrity: sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==} + dev: false + /undici/5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} diff --git a/www/docs/src/content/docs/en/api-reference.md b/www/docs/src/content/docs/en/api-reference.md index 78d3b8e..3a468d2 100644 --- a/www/docs/src/content/docs/en/api-reference.md +++ b/www/docs/src/content/docs/en/api-reference.md @@ -209,7 +209,7 @@ making sure you pass the correct validator for the current route. ```ts declare function useSearchParams>( - searchValidator: T + searchValidator: T, ): UseParamsResult; ``` @@ -221,7 +221,7 @@ making sure you pass the correct validator for the current route. ```ts declare function useRouteParams>( - validator: T + validator: T, ): UseParamsResult; ``` @@ -244,7 +244,7 @@ It should be the default export of `page.tsx`. ```ts declare function withParamValidation( Component: SomeReactComponent, - validator: DynamicRoute + validator: DynamicRoute, ): SomeReactComponent; ``` @@ -257,7 +257,7 @@ The component you wrap with this should use `InferLayoutPropsType` for its props ```ts declare function withLayoutParamValidation( Component: SomeReactComponent, - validator: DynamicLayout + validator: DynamicLayout, ): SomeReactComponent; ``` @@ -270,7 +270,7 @@ Should only be used in the top level route component where your `Route` object i ```ts declare function useSearchParams>( - searchValidator: T + searchValidator: T, ): UseParamsResult; ``` @@ -281,7 +281,7 @@ Should only be used in the top level route component where your `Route` object i ```ts declare function useRouteParams>( - validator: T + validator: T, ): UseParamsResult; ```