one 1.2.16 → 1.2.17

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.
Files changed (125) hide show
  1. package/dist/cjs/cli/generateRoutes.cjs +20 -5
  2. package/dist/cjs/cli/generateRoutes.js +18 -3
  3. package/dist/cjs/cli/generateRoutes.js.map +1 -1
  4. package/dist/cjs/cli/generateRoutes.native.js +30 -5
  5. package/dist/cjs/cli/generateRoutes.native.js.map +1 -1
  6. package/dist/cjs/cli.cjs +4 -0
  7. package/dist/cjs/cli.js +4 -0
  8. package/dist/cjs/cli.js.map +1 -1
  9. package/dist/cjs/cli.native.js +4 -0
  10. package/dist/cjs/cli.native.js.map +1 -1
  11. package/dist/cjs/index.js.map +1 -1
  12. package/dist/cjs/index.native.js.map +1 -1
  13. package/dist/cjs/router/createRoute.cjs +12 -1
  14. package/dist/cjs/router/createRoute.js +12 -1
  15. package/dist/cjs/router/createRoute.js.map +1 -1
  16. package/dist/cjs/router/createRoute.native.js +13 -2
  17. package/dist/cjs/router/createRoute.native.js.map +1 -1
  18. package/dist/cjs/typed-routes/generateRouteTypes.cjs +14 -3
  19. package/dist/cjs/typed-routes/generateRouteTypes.js +12 -3
  20. package/dist/cjs/typed-routes/generateRouteTypes.js.map +1 -1
  21. package/dist/cjs/typed-routes/generateRouteTypes.native.js +29 -3
  22. package/dist/cjs/typed-routes/generateRouteTypes.native.js.map +1 -1
  23. package/dist/cjs/typed-routes/getTypedRoutesDeclarationFile.cjs +39 -2
  24. package/dist/cjs/typed-routes/getTypedRoutesDeclarationFile.js +38 -2
  25. package/dist/cjs/typed-routes/getTypedRoutesDeclarationFile.js.map +1 -1
  26. package/dist/cjs/typed-routes/getTypedRoutesDeclarationFile.native.js +40 -2
  27. package/dist/cjs/typed-routes/getTypedRoutesDeclarationFile.native.js.map +1 -1
  28. package/dist/cjs/typed-routes/injectRouteHelpers.cjs +136 -0
  29. package/dist/cjs/typed-routes/injectRouteHelpers.js +104 -0
  30. package/dist/cjs/typed-routes/injectRouteHelpers.js.map +6 -0
  31. package/dist/cjs/typed-routes/injectRouteHelpers.native.js +146 -0
  32. package/dist/cjs/typed-routes/injectRouteHelpers.native.js.map +1 -0
  33. package/dist/cjs/vite/loadConfig.cjs +18 -12
  34. package/dist/cjs/vite/loadConfig.js +20 -13
  35. package/dist/cjs/vite/loadConfig.js.map +1 -1
  36. package/dist/cjs/vite/loadConfig.native.js +18 -11
  37. package/dist/cjs/vite/loadConfig.native.js.map +1 -1
  38. package/dist/cjs/vite/plugins/generateFileSystemRouteTypesPlugin.cjs +3 -2
  39. package/dist/cjs/vite/plugins/generateFileSystemRouteTypesPlugin.js +13 -3
  40. package/dist/cjs/vite/plugins/generateFileSystemRouteTypesPlugin.js.map +1 -1
  41. package/dist/cjs/vite/plugins/generateFileSystemRouteTypesPlugin.native.js +9 -6
  42. package/dist/cjs/vite/plugins/generateFileSystemRouteTypesPlugin.native.js.map +1 -1
  43. package/dist/esm/cli/generateRoutes.js +19 -2
  44. package/dist/esm/cli/generateRoutes.js.map +1 -1
  45. package/dist/esm/cli/generateRoutes.mjs +19 -4
  46. package/dist/esm/cli/generateRoutes.mjs.map +1 -1
  47. package/dist/esm/cli/generateRoutes.native.js +29 -4
  48. package/dist/esm/cli/generateRoutes.native.js.map +1 -1
  49. package/dist/esm/cli.js +4 -0
  50. package/dist/esm/cli.js.map +1 -1
  51. package/dist/esm/cli.mjs +4 -0
  52. package/dist/esm/cli.mjs.map +1 -1
  53. package/dist/esm/cli.native.js +4 -0
  54. package/dist/esm/cli.native.js.map +1 -1
  55. package/dist/esm/index.js.map +1 -1
  56. package/dist/esm/index.mjs.map +1 -1
  57. package/dist/esm/index.native.js.map +1 -1
  58. package/dist/esm/router/createRoute.js +12 -1
  59. package/dist/esm/router/createRoute.js.map +1 -1
  60. package/dist/esm/router/createRoute.mjs +12 -1
  61. package/dist/esm/router/createRoute.mjs.map +1 -1
  62. package/dist/esm/router/createRoute.native.js +13 -2
  63. package/dist/esm/router/createRoute.native.js.map +1 -1
  64. package/dist/esm/typed-routes/generateRouteTypes.js +14 -3
  65. package/dist/esm/typed-routes/generateRouteTypes.js.map +1 -1
  66. package/dist/esm/typed-routes/generateRouteTypes.mjs +14 -3
  67. package/dist/esm/typed-routes/generateRouteTypes.mjs.map +1 -1
  68. package/dist/esm/typed-routes/generateRouteTypes.native.js +29 -3
  69. package/dist/esm/typed-routes/generateRouteTypes.native.js.map +1 -1
  70. package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.js +38 -2
  71. package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.js.map +1 -1
  72. package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.mjs +39 -2
  73. package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.mjs.map +1 -1
  74. package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.native.js +40 -2
  75. package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.native.js.map +1 -1
  76. package/dist/esm/typed-routes/injectRouteHelpers.js +89 -0
  77. package/dist/esm/typed-routes/injectRouteHelpers.js.map +6 -0
  78. package/dist/esm/typed-routes/injectRouteHelpers.mjs +113 -0
  79. package/dist/esm/typed-routes/injectRouteHelpers.mjs.map +1 -0
  80. package/dist/esm/typed-routes/injectRouteHelpers.native.js +120 -0
  81. package/dist/esm/typed-routes/injectRouteHelpers.native.js.map +1 -0
  82. package/dist/esm/vite/loadConfig.js +20 -13
  83. package/dist/esm/vite/loadConfig.js.map +1 -1
  84. package/dist/esm/vite/loadConfig.mjs +18 -12
  85. package/dist/esm/vite/loadConfig.mjs.map +1 -1
  86. package/dist/esm/vite/loadConfig.native.js +18 -11
  87. package/dist/esm/vite/loadConfig.native.js.map +1 -1
  88. package/dist/esm/vite/plugins/generateFileSystemRouteTypesPlugin.js +13 -3
  89. package/dist/esm/vite/plugins/generateFileSystemRouteTypesPlugin.js.map +1 -1
  90. package/dist/esm/vite/plugins/generateFileSystemRouteTypesPlugin.mjs +3 -2
  91. package/dist/esm/vite/plugins/generateFileSystemRouteTypesPlugin.mjs.map +1 -1
  92. package/dist/esm/vite/plugins/generateFileSystemRouteTypesPlugin.native.js +9 -6
  93. package/dist/esm/vite/plugins/generateFileSystemRouteTypesPlugin.native.js.map +1 -1
  94. package/package.json +10 -10
  95. package/src/cli/generateRoutes.ts +52 -4
  96. package/src/cli.ts +5 -0
  97. package/src/index.ts +13 -0
  98. package/src/interfaces/router.ts +19 -0
  99. package/src/router/createRoute.ts +16 -3
  100. package/src/typed-routes/generateRouteTypes.ts +46 -2
  101. package/src/typed-routes/getTypedRoutesDeclarationFile.ts +70 -0
  102. package/src/typed-routes/injectRouteHelpers.ts +186 -0
  103. package/src/vite/loadConfig.ts +29 -17
  104. package/src/vite/plugins/generateFileSystemRouteTypesPlugin.tsx +14 -3
  105. package/src/vite/types.ts +26 -0
  106. package/types/cli/generateRoutes.d.ts +1 -0
  107. package/types/cli/generateRoutes.d.ts.map +1 -1
  108. package/types/index.d.ts +13 -3
  109. package/types/index.d.ts.map +1 -1
  110. package/types/interfaces/router.d.ts +16 -0
  111. package/types/interfaces/router.d.ts.map +1 -1
  112. package/types/router/createRoute.d.ts +28 -13
  113. package/types/router/createRoute.d.ts.map +1 -1
  114. package/types/typed-routes/generateRouteTypes.d.ts +1 -1
  115. package/types/typed-routes/generateRouteTypes.d.ts.map +1 -1
  116. package/types/typed-routes/getTypedRoutesDeclarationFile.d.ts.map +1 -1
  117. package/types/typed-routes/injectRouteHelpers.d.ts +12 -0
  118. package/types/typed-routes/injectRouteHelpers.d.ts.map +1 -0
  119. package/types/utils/redirect.d.ts +1 -3
  120. package/types/utils/redirect.d.ts.map +1 -1
  121. package/types/vite/loadConfig.d.ts +1 -1
  122. package/types/vite/loadConfig.d.ts.map +1 -1
  123. package/types/vite/plugins/generateFileSystemRouteTypesPlugin.d.ts.map +1 -1
  124. package/types/vite/types.d.ts +25 -0
  125. package/types/vite/types.d.ts.map +1 -1
package/src/cli.ts CHANGED
@@ -251,6 +251,11 @@ const generateRoutes = defineCommand({
251
251
  type: 'string',
252
252
  description: 'Path to app directory (default: "app")',
253
253
  },
254
+ typed: {
255
+ type: 'string',
256
+ description:
257
+ 'Auto-generate route helpers. Options: "type" (type-only helpers) or "runtime" (runtime helpers)',
258
+ },
254
259
  },
255
260
  async run({ args }) {
256
261
  const { run } = await import('./cli/generateRoutes')
package/src/index.ts CHANGED
@@ -9,6 +9,19 @@ export type Href = '__branded__' extends keyof OneRouter.Href ? string : OneRout
9
9
 
10
10
  export type LinkProps<T extends string | object = string> = OneRouter.LinkProps<T>
11
11
 
12
+ /**
13
+ * Helper type to get route information including params and loader props.
14
+ * Can be overridden in generated routes.d.ts for per-route types.
15
+ *
16
+ * @example
17
+ * import type { RouteType } from 'one'
18
+ *
19
+ * type MyRoute = RouteType<'(site)/docs/[slug]'>
20
+ * // MyRoute.Params = { slug: string }
21
+ * // MyRoute.LoaderProps = { params: { slug: string }, path: string, request?: Request }
22
+ */
23
+ export type RouteType<Path extends string = string> = OneRouter.RouteType<Path>
24
+
12
25
  // hooks
13
26
  export { useIsFocused } from '@react-navigation/core'
14
27
  // re-export
@@ -16,6 +16,25 @@ export namespace OneRouter {
16
16
  Loader: (props: { params: InputRouteParams<Path> }) => any
17
17
  }
18
18
 
19
+ /**
20
+ * Helper type to get route information including params and loader props.
21
+ * Uses generated RouteTypes from routes.d.ts if available for better intellisense.
22
+ *
23
+ * @example
24
+ * const route = createRoute<'/docs/[slug]'>()
25
+ * // route.createLoader gets params typed as { slug: string }
26
+ *
27
+ * type Route = RouteType<'/docs/[slug]'>
28
+ * // Route.Params = { slug: string }
29
+ * // Route.LoaderProps = { path: string; params: { slug: string }; request?: Request }
30
+ */
31
+ export type RouteType<Path extends string> = Path extends keyof __routes['RouteTypes']
32
+ ? __routes['RouteTypes'][Path]
33
+ : {
34
+ Params: InputRouteParams<Path>
35
+ LoaderProps: import('../types').LoaderProps<InputRouteParams<Path>>
36
+ }
37
+
19
38
  type StaticRoutes = __routes extends { StaticRoutes: string } ? __routes['StaticRoutes'] : string
20
39
 
21
40
  type DynamicRoutes<T extends string> = __routes<T> extends { DynamicRoutes: any }
@@ -1,14 +1,27 @@
1
1
  import { useActiveParams, useParams, usePathname } from '../hooks'
2
2
  import type { OneRouter } from '../interfaces/router'
3
+ import type { LoaderProps } from '../types'
3
4
 
4
- export function createRoute<Path>() {
5
- type Route = OneRouter.Route<Path>
5
+ export function createRoute<Path extends string = string>() {
6
+ type Route = OneRouter.RouteType<Path>
6
7
  type Params = Route['Params']
8
+ type TypedLoaderProps = LoaderProps<Params>
7
9
 
8
10
  return {
9
11
  useParams: () => useParams<Params>(),
10
12
  useActiveParams: () => useActiveParams<Params>(),
11
- createLoader: (a: Route['Loader']) => a,
13
+ /**
14
+ * Creates a typed loader function for this route.
15
+ * The loader receives LoaderProps with typed params.
16
+ *
17
+ * @example
18
+ * const route = createRoute<'(site)/docs/[slug]'>()
19
+ * export const loader = route.createLoader(({ params }) => {
20
+ * // params is typed as { slug: string }
21
+ * return { doc: getDoc(params.slug) }
22
+ * })
23
+ */
24
+ createLoader: <T>(fn: (props: TypedLoaderProps) => T) => fn,
12
25
  }
13
26
  }
14
27
 
@@ -1,15 +1,19 @@
1
1
  import { writeFile } from 'node:fs/promises'
2
- import { dirname } from 'node:path'
2
+ import { dirname, join } from 'node:path'
3
3
  import FSExtra from 'fs-extra'
4
4
  import micromatch from 'micromatch'
5
5
  import { globbedRoutesToRouteContext } from '../router/useViteRoutes'
6
6
  import { globDir } from '../utils/globDir'
7
+ import type { One } from '../vite/types'
7
8
  import { getTypedRoutesDeclarationFile } from './getTypedRoutesDeclarationFile'
9
+ import { injectRouteHelpers, type InjectMode } from './injectRouteHelpers'
10
+ import { removeSupportedExtensions } from '../router/matchers'
8
11
 
9
12
  export async function generateRouteTypes(
10
13
  outFile: string,
11
14
  routerRoot: string,
12
- ignoredRouteFiles?: string[]
15
+ ignoredRouteFiles?: string[],
16
+ typedRoutesMode?: 'type' | 'runtime'
13
17
  ) {
14
18
  let routePaths = globDir(routerRoot)
15
19
  if (ignoredRouteFiles && ignoredRouteFiles.length > 0) {
@@ -27,4 +31,44 @@ export async function generateRouteTypes(
27
31
  const outDir = dirname(outFile)
28
32
  await FSExtra.ensureDir(outDir)
29
33
  await writeFile(outFile, declarations)
34
+
35
+ // If experimental.typedRoutesGeneration is enabled, inject helpers into route files
36
+ if (typedRoutesMode) {
37
+ const mode: InjectMode = typedRoutesMode === 'type' ? 'type' : 'runtime'
38
+
39
+ // Inject helpers into each route file
40
+ for (const routePath of routePaths) {
41
+ // Skip non-route files (layouts, middlewares, type definitions, etc.)
42
+ if (
43
+ routePath.includes('_layout') ||
44
+ routePath.includes('+api') ||
45
+ routePath.startsWith('_') ||
46
+ routePath.endsWith('.d.ts')
47
+ ) {
48
+ continue
49
+ }
50
+
51
+ // Convert route path to route name
52
+ // e.g., "./app/(site)/docs/[slug]+ssg.tsx" -> "/(site)/docs/[slug]"
53
+ const fullPath = join(process.cwd(), routerRoot, routePath)
54
+ const routeName = routePath
55
+ .replace(/^\.\//, '')
56
+ .replace(/\+[^/]*$/, '') // Remove +ssg, +ssr, etc.
57
+ .replace(/\/index$/, '')
58
+ .replace(/index$/, '')
59
+ let cleanRouteName = removeSupportedExtensions(routeName).replace(/\/?index$/, '')
60
+
61
+ // Ensure leading slash
62
+ if (!cleanRouteName.startsWith('/')) {
63
+ cleanRouteName = '/' + cleanRouteName
64
+ }
65
+
66
+ // Skip routes without dynamic segments (no params to type)
67
+ if (!cleanRouteName.includes('[')) {
68
+ continue
69
+ }
70
+
71
+ await injectRouteHelpers(fullPath, cleanRouteName, mode)
72
+ }
73
+ }
30
74
  }
@@ -26,6 +26,8 @@ export function getTypedRoutesDeclarationFile(ctx: One.RouteContext) {
26
26
  dynamicRouteContextKeys
27
27
  )
28
28
 
29
+ const hasRoutes = dynamicRouteContextKeys.size > 0
30
+
29
31
  return `// deno-lint-ignore-file
30
32
  /* eslint-disable */
31
33
  // biome-ignore: needed import
@@ -38,12 +40,80 @@ declare module 'one' {
38
40
  DynamicRoutes: ${setToUnionType(dynamicRoutes)}
39
41
  DynamicRouteTemplate: ${setToUnionType(dynamicRouteContextKeys)}
40
42
  IsTyped: true
43
+ ${hasRoutes ? `RouteTypes: ${generateRouteTypesMap(dynamicRouteContextKeys)}` : ''}
41
44
  }
42
45
  }
43
46
  }
47
+ ${
48
+ hasRoutes
49
+ ? `
50
+ /**
51
+ * Helper type for route information
52
+ */
53
+ type RouteInfo<Params = Record<string, never>> = {
54
+ Params: Params
55
+ LoaderProps: { path: string; params: Params; request?: Request }
56
+ }`
57
+ : ''
58
+ }
44
59
  `.trim()
45
60
  }
46
61
 
62
+ /**
63
+ * Generates a mapped type for all routes with their expanded types
64
+ * This improves intellisense by showing actual param types instead of aliases
65
+ */
66
+ function generateRouteTypesMap(dynamicRouteContextKeys: Set<string>): string {
67
+ if (dynamicRouteContextKeys.size === 0) {
68
+ return '{}'
69
+ }
70
+
71
+ const routes = [...dynamicRouteContextKeys].sort()
72
+
73
+ const entries = routes
74
+ .map((routePath) => {
75
+ // Generate the param type inline for better intellisense
76
+ const params = extractParams(routePath)
77
+ const paramsType = params.length === 0 ? '{}' : generateInlineParamsType(params)
78
+
79
+ return ` '${routePath}': RouteInfo<${paramsType}>`
80
+ })
81
+ .join('\n')
82
+
83
+ return `{\n${entries}\n }`
84
+ }
85
+
86
+ /**
87
+ * Extract parameter names from a route path
88
+ * e.g., "/docs/[slug]/[id]" -> ["slug", "id"]
89
+ */
90
+ function extractParams(routePath: string): Array<{ name: string; isCatchAll: boolean }> {
91
+ const params: Array<{ name: string; isCatchAll: boolean }> = []
92
+ const paramRegex = /\[(\.\.\.)?([\w]+)\]/g
93
+ let match
94
+
95
+ while ((match = paramRegex.exec(routePath)) !== null) {
96
+ params.push({
97
+ name: match[2],
98
+ isCatchAll: match[1] === '...',
99
+ })
100
+ }
101
+
102
+ return params
103
+ }
104
+
105
+ /**
106
+ * Generate inline params type for better intellisense
107
+ * e.g., [{ name: "slug", isCatchAll: false }] -> "{ slug: string }"
108
+ */
109
+ function generateInlineParamsType(params: Array<{ name: string; isCatchAll: boolean }>): string {
110
+ const entries = params.map((p) => {
111
+ const type = p.isCatchAll ? 'string[]' : 'string'
112
+ return `${p.name}: ${type}`
113
+ })
114
+ return `{ ${entries.join('; ')} }`
115
+ }
116
+
47
117
  /**
48
118
  * Walks a RouteNode tree and adds the routes to the provided sets
49
119
  */
@@ -0,0 +1,186 @@
1
+ import { readFile, writeFile } from 'node:fs/promises'
2
+ import { existsSync } from 'node:fs'
3
+
4
+ export type InjectMode = 'type' | 'runtime'
5
+
6
+ /**
7
+ * Injects route type helpers into a route file if they don't already exist.
8
+ *
9
+ * This function:
10
+ * - Checks if the file already has `type Route` or `const route` declarations
11
+ * - Adds them if missing with proper spacing (blank line after imports)
12
+ * - Tries to add imports to existing `import {} from 'one'` statements
13
+ * - Does NOT modify existing loader code - that's up to the user
14
+ */
15
+ export async function injectRouteHelpers(
16
+ filePath: string,
17
+ routePath: string,
18
+ mode: InjectMode
19
+ ): Promise<boolean> {
20
+ if (!existsSync(filePath)) {
21
+ return false
22
+ }
23
+
24
+ try {
25
+ let content = await readFile(filePath, 'utf-8')
26
+ let modified = false
27
+
28
+ // Check if already has type Route or const route
29
+ const hasTypeRoute = /^type\s+Route\s*=/m.test(content)
30
+ const hasConstRoute = /^const\s+route\s*=/m.test(content)
31
+
32
+ // If runtime mode and doesn't have const route, add it
33
+ if (mode === 'runtime' && !hasConstRoute) {
34
+ const { updatedContent } = addCreateRouteImport(content)
35
+ content = updatedContent
36
+
37
+ // Add const route declaration after imports with blank line before
38
+ const routeDeclaration = `const route = createRoute<'${routePath}'>()`
39
+ content = insertAfterImports(content, routeDeclaration)
40
+ modified = true
41
+ }
42
+
43
+ // If type mode and doesn't have type Route, add it
44
+ if (mode === 'type' && !hasTypeRoute) {
45
+ const { updatedContent } = addRouteTypeImport(content)
46
+ content = updatedContent
47
+
48
+ // Add type Route declaration after imports with blank line before
49
+ const typeDeclaration = `type Route = RouteType<'${routePath}'>`
50
+ content = insertAfterImports(content, typeDeclaration)
51
+ modified = true
52
+ }
53
+
54
+ if (modified) {
55
+ await writeFile(filePath, content, 'utf-8')
56
+ return true
57
+ }
58
+
59
+ return false
60
+ } catch (error) {
61
+ console.error(`Failed to inject route helpers into ${filePath}:`, error)
62
+ return false
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Adds createRoute to an existing import from 'one', or creates a new import
68
+ */
69
+ function addCreateRouteImport(content: string): { updatedContent: string; importAdded: boolean } {
70
+ // Check if already imports createRoute
71
+ if (/import\s+[^'"]*createRoute[^'"]*from\s+['"]one['"]/m.test(content)) {
72
+ return { updatedContent: content, importAdded: false }
73
+ }
74
+
75
+ // Try to find existing import from 'one'
76
+ const oneImportRegex = /import\s+{([^}]*)}\s+from\s+['"]one['"]/m
77
+ const match = content.match(oneImportRegex)
78
+
79
+ if (match) {
80
+ // Add createRoute to existing import
81
+ const existingImports = match[1].trim()
82
+ const newImports = existingImports ? `${existingImports}, createRoute` : 'createRoute'
83
+ const updatedContent = content.replace(oneImportRegex, `import { ${newImports} } from 'one'`)
84
+ return { updatedContent, importAdded: true }
85
+ }
86
+
87
+ // No existing import, add a new one after the last import
88
+ const lastImportIndex = findLastImportIndex(content)
89
+ if (lastImportIndex >= 0) {
90
+ const lines = content.split('\n')
91
+ lines.splice(lastImportIndex + 1, 0, `import { createRoute } from 'one'`)
92
+ return { updatedContent: lines.join('\n'), importAdded: true }
93
+ }
94
+
95
+ // No imports at all, add at the top
96
+ const newImport = `import { createRoute } from 'one'\n`
97
+ return { updatedContent: newImport + content, importAdded: true }
98
+ }
99
+
100
+ /**
101
+ * Adds RouteType to an existing type import from 'one', or creates a new import
102
+ */
103
+ function addRouteTypeImport(content: string): { updatedContent: string; importAdded: boolean } {
104
+ // Check if already imports RouteType
105
+ if (/import\s+type\s+[^'"]*RouteType[^'"]*from\s+['"]one['"]/m.test(content)) {
106
+ return { updatedContent: content, importAdded: false }
107
+ }
108
+
109
+ // Try to find existing type import from 'one'
110
+ const oneTypeImportRegex = /import\s+type\s+{([^}]*)}\s+from\s+['"]one['"]/m
111
+ const match = content.match(oneTypeImportRegex)
112
+
113
+ if (match) {
114
+ // Add RouteType to existing import
115
+ const existingImports = match[1].trim()
116
+ const newImports = existingImports ? `${existingImports}, RouteType` : 'RouteType'
117
+ const updatedContent = content.replace(
118
+ oneTypeImportRegex,
119
+ `import type { ${newImports} } from 'one'`
120
+ )
121
+ return { updatedContent, importAdded: true }
122
+ }
123
+
124
+ // No existing type import, add a new one after the last import
125
+ const lastImportIndex = findLastImportIndex(content)
126
+ if (lastImportIndex >= 0) {
127
+ const lines = content.split('\n')
128
+ lines.splice(lastImportIndex + 1, 0, `import type { RouteType } from 'one'`)
129
+ return { updatedContent: lines.join('\n'), importAdded: true }
130
+ }
131
+
132
+ // No imports at all, add at the top
133
+ const newImport = `import type { RouteType } from 'one'\n`
134
+ return { updatedContent: newImport + content, importAdded: true }
135
+ }
136
+
137
+ /**
138
+ * Finds the index of the last import statement line
139
+ */
140
+ function findLastImportIndex(content: string): number {
141
+ const lines = content.split('\n')
142
+ let lastImportIndex = -1
143
+
144
+ for (let i = 0; i < lines.length; i++) {
145
+ const line = lines[i].trim()
146
+ if (
147
+ line.startsWith('import ') ||
148
+ (lastImportIndex >= 0 && (line.startsWith('from ') || line === '}'))
149
+ ) {
150
+ lastImportIndex = i
151
+ } else if (lastImportIndex >= 0 && line && !line.startsWith('//')) {
152
+ // Stop once we hit non-import code
153
+ break
154
+ }
155
+ }
156
+
157
+ return lastImportIndex
158
+ }
159
+
160
+ /**
161
+ * Inserts code after the last import statement with proper spacing
162
+ * Ensures there's a blank line between imports and the inserted code, and after the inserted code
163
+ */
164
+ function insertAfterImports(content: string, codeToInsert: string): string {
165
+ const lines = content.split('\n')
166
+ const lastImportIndex = findLastImportIndex(content)
167
+
168
+ if (lastImportIndex >= 0) {
169
+ // Check if there's already a blank line after imports
170
+ const nextLine = lines[lastImportIndex + 1]
171
+ const hasBlankLine = nextLine === ''
172
+
173
+ if (hasBlankLine) {
174
+ // Insert after the blank line with a blank line after
175
+ lines.splice(lastImportIndex + 2, 0, codeToInsert, '')
176
+ } else {
177
+ // Add blank line before and after code
178
+ lines.splice(lastImportIndex + 1, 0, '', codeToInsert, '')
179
+ }
180
+
181
+ return lines.join('\n')
182
+ }
183
+
184
+ // No imports found, add at the beginning with spacing
185
+ return codeToInsert + '\n\n' + content
186
+ }
@@ -15,24 +15,36 @@ function getUserOneOptions() {
15
15
  return globalThis.__oneOptions as One.PluginOptions
16
16
  }
17
17
 
18
- export async function loadUserOneOptions(command: 'serve' | 'build') {
19
- const config = await loadConfigFromFile({
20
- mode: command === 'serve' ? 'dev' : 'prod',
21
- command,
22
- })
23
-
24
- if (!config) {
25
- throw new Error(`No config config in ${process.cwd()}. Is this the correct directory?`)
18
+ export async function loadUserOneOptions(command: 'serve' | 'build', silent = false) {
19
+ // Suppress console output if silent
20
+ const originalConsoleError = console.error
21
+ if (silent) {
22
+ console.error = () => {}
26
23
  }
27
24
 
28
- const oneOptions = getUserOneOptions()
29
-
30
- if (!oneOptions) {
31
- throw new Error(`No One plugin config in this vite.config`)
32
- }
33
-
34
- return {
35
- config,
36
- oneOptions,
25
+ try {
26
+ const config = await loadConfigFromFile({
27
+ mode: command === 'serve' ? 'dev' : 'prod',
28
+ command,
29
+ })
30
+
31
+ if (!config) {
32
+ throw new Error(`No config config in ${process.cwd()}. Is this the correct directory?`)
33
+ }
34
+
35
+ const oneOptions = getUserOneOptions()
36
+
37
+ if (!oneOptions) {
38
+ throw new Error(`No One plugin config in this vite.config`)
39
+ }
40
+
41
+ return {
42
+ config,
43
+ oneOptions,
44
+ }
45
+ } finally {
46
+ if (silent) {
47
+ console.error = originalConsoleError
48
+ }
37
49
  }
38
50
  }
@@ -17,13 +17,19 @@ export function generateFileSystemRouteTypesPlugin(options: One.PluginOptions):
17
17
  const outFile = join(appDir, 'routes.d.ts')
18
18
 
19
19
  const routerRoot = getRouterRootFromOneOptions(options)
20
+ const typedRoutesGeneration = options.router?.experimental?.typedRoutesGeneration || undefined
20
21
 
21
22
  // on change ./app stuff lets reload this to pick up any route changes
22
23
  const fileWatcherChangeListener = debounce(async (type: string, path: string) => {
23
- if (type === 'add' || type === 'delete') {
24
+ if (type === 'add' || type === 'delete' || type === 'change') {
24
25
  if (path.startsWith(appDir)) {
25
26
  // generate
26
- generateRouteTypes(outFile, routerRoot, options.router?.ignoredRouteFiles)
27
+ generateRouteTypes(
28
+ outFile,
29
+ routerRoot,
30
+ options.router?.ignoredRouteFiles,
31
+ typedRoutesGeneration
32
+ )
27
33
  }
28
34
  }
29
35
  }, 100)
@@ -33,7 +39,12 @@ export function generateFileSystemRouteTypesPlugin(options: One.PluginOptions):
33
39
  return () => {
34
40
  // once on startup:
35
41
 
36
- generateRouteTypes(outFile, routerRoot, options.router?.ignoredRouteFiles)
42
+ generateRouteTypes(
43
+ outFile,
44
+ routerRoot,
45
+ options.router?.ignoredRouteFiles,
46
+ typedRoutesGeneration
47
+ )
37
48
  }
38
49
  },
39
50
  } satisfies Plugin
package/src/vite/types.ts CHANGED
@@ -136,6 +136,32 @@ export namespace One {
136
136
  * Currently, this will only effect the `<Slot />` navigator, where it will modify the screen element provided by `react-navigation` and set the `key` to a static value to prevent re-mounting.
137
137
  */
138
138
  preventLayoutRemounting?: boolean
139
+
140
+ /**
141
+ * Auto-generate route type helpers in route files.
142
+ *
143
+ * Route types are always generated in routes.d.ts. This option controls whether
144
+ * One automatically inserts type helpers into your route files.
145
+ *
146
+ * Options:
147
+ * - `false` (default): No auto-generation, manually add types yourself
148
+ * - `'type'`: Auto-inserts type-only helpers:
149
+ * ```typescript
150
+ * import type { RouteType } from 'one'
151
+ * type Route = RouteType<'/your/[route]'>
152
+ * ```
153
+ * - `'runtime'`: Auto-inserts runtime helpers:
154
+ * ```typescript
155
+ * import { createRoute } from 'one'
156
+ * const route = createRoute<'/your/[route]'>()
157
+ * ```
158
+ *
159
+ * The insertion happens automatically when route files are created or modified,
160
+ * and respects your existing code (won't modify loaders, etc).
161
+ *
162
+ * @default false
163
+ */
164
+ typedRoutesGeneration?: false | 'type' | 'runtime'
139
165
  }
140
166
  }
141
167
 
@@ -1,4 +1,5 @@
1
1
  export declare function run(args?: {
2
2
  appDir?: string;
3
+ typed?: string;
3
4
  }): Promise<void>;
4
5
  //# sourceMappingURL=generateRoutes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generateRoutes.d.ts","sourceRoot":"","sources":["../../src/cli/generateRoutes.ts"],"names":[],"mappings":"AAIA,wBAAsB,GAAG,CAAC,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,iBAkBvD"}
1
+ {"version":3,"file":"generateRoutes.d.ts","sourceRoot":"","sources":["../../src/cli/generateRoutes.ts"],"names":[],"mappings":"AAMA,wBAAsB,GAAG,CAAC,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,iBAgEvE"}
package/types/index.d.ts CHANGED
@@ -1,10 +1,20 @@
1
1
  export { createApp } from './createApp';
2
2
  export type { One, OneRouter } from './interfaces/router';
3
3
  import type { OneRouter } from './interfaces/router';
4
- export type Href = OneRouter.Href extends {
5
- __branded__: any;
6
- } ? string : OneRouter.Href;
4
+ export type Href = '__branded__' extends keyof OneRouter.Href ? string : OneRouter.Href;
7
5
  export type LinkProps<T extends string | object = string> = OneRouter.LinkProps<T>;
6
+ /**
7
+ * Helper type to get route information including params and loader props.
8
+ * Can be overridden in generated routes.d.ts for per-route types.
9
+ *
10
+ * @example
11
+ * import type { RouteType } from 'one'
12
+ *
13
+ * type MyRoute = RouteType<'(site)/docs/[slug]'>
14
+ * // MyRoute.Params = { slug: string }
15
+ * // MyRoute.LoaderProps = { params: { slug: string }, path: string, request?: Request }
16
+ */
17
+ export type RouteType<Path extends string = string> = OneRouter.RouteType<Path>;
8
18
  export { useIsFocused } from '@react-navigation/core';
9
19
  export * from '@vxrn/universal-color-scheme';
10
20
  export { SafeAreaView } from 'react-native-safe-area-context';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEvC,YAAY,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAGpD,MAAM,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,SAAS;IAAE,WAAW,EAAE,GAAG,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CAAC,IAAI,CAAA;AAExF,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,MAAM,IAAI,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAGlF,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAErD,cAAc,8BAA8B,CAAA;AAI5C,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAG9D,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACtE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAE7B,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,yBAAyB,EACzB,SAAS,EACT,WAAW,EACX,sBAAsB,EACtB,SAAS,EACT,WAAW,EACX,qBAAqB,GACtB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAE7B,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAA;AAErC,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAE7B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAEzD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAChD,OAAO,KAAK,WAAW,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACtD,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,KAAK,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AACnG,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEvD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEvC,YAAY,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAGpD,MAAM,MAAM,IAAI,GAAG,aAAa,SAAS,MAAM,SAAS,CAAC,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC,IAAI,CAAA;AAEvF,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,MAAM,IAAI,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAElF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,SAAS,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,IAAI,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;AAG/E,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAErD,cAAc,8BAA8B,CAAA;AAI5C,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAG9D,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACtE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAE7B,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,yBAAyB,EACzB,SAAS,EACT,WAAW,EACX,sBAAsB,EACtB,SAAS,EACT,WAAW,EACX,qBAAqB,GACtB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAE7B,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAA;AAErC,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAE7B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAEzD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAChD,OAAO,KAAK,WAAW,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACtD,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,KAAK,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AACnG,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEvD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA"}
@@ -13,6 +13,22 @@ export declare namespace OneRouter {
13
13
  params: InputRouteParams<Path>;
14
14
  }) => any;
15
15
  };
16
+ /**
17
+ * Helper type to get route information including params and loader props.
18
+ * Uses generated RouteTypes from routes.d.ts if available for better intellisense.
19
+ *
20
+ * @example
21
+ * const route = createRoute<'/docs/[slug]'>()
22
+ * // route.createLoader gets params typed as { slug: string }
23
+ *
24
+ * type Route = RouteType<'/docs/[slug]'>
25
+ * // Route.Params = { slug: string }
26
+ * // Route.LoaderProps = { path: string; params: { slug: string }; request?: Request }
27
+ */
28
+ export type RouteType<Path extends string> = Path extends keyof __routes['RouteTypes'] ? __routes['RouteTypes'][Path] : {
29
+ Params: InputRouteParams<Path>;
30
+ LoaderProps: import('../types').LoaderProps<InputRouteParams<Path>>;
31
+ };
16
32
  type StaticRoutes = __routes extends {
17
33
  StaticRoutes: string;
18
34
  } ? __routes['StaticRoutes'] : string;