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.
- package/dist/cjs/cli/generateRoutes.cjs +20 -5
- package/dist/cjs/cli/generateRoutes.js +18 -3
- package/dist/cjs/cli/generateRoutes.js.map +1 -1
- package/dist/cjs/cli/generateRoutes.native.js +30 -5
- package/dist/cjs/cli/generateRoutes.native.js.map +1 -1
- package/dist/cjs/cli.cjs +4 -0
- package/dist/cjs/cli.js +4 -0
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/cli.native.js +4 -0
- package/dist/cjs/cli.native.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/router/createRoute.cjs +12 -1
- package/dist/cjs/router/createRoute.js +12 -1
- package/dist/cjs/router/createRoute.js.map +1 -1
- package/dist/cjs/router/createRoute.native.js +13 -2
- package/dist/cjs/router/createRoute.native.js.map +1 -1
- package/dist/cjs/typed-routes/generateRouteTypes.cjs +14 -3
- package/dist/cjs/typed-routes/generateRouteTypes.js +12 -3
- package/dist/cjs/typed-routes/generateRouteTypes.js.map +1 -1
- package/dist/cjs/typed-routes/generateRouteTypes.native.js +29 -3
- package/dist/cjs/typed-routes/generateRouteTypes.native.js.map +1 -1
- package/dist/cjs/typed-routes/getTypedRoutesDeclarationFile.cjs +39 -2
- package/dist/cjs/typed-routes/getTypedRoutesDeclarationFile.js +38 -2
- package/dist/cjs/typed-routes/getTypedRoutesDeclarationFile.js.map +1 -1
- package/dist/cjs/typed-routes/getTypedRoutesDeclarationFile.native.js +40 -2
- package/dist/cjs/typed-routes/getTypedRoutesDeclarationFile.native.js.map +1 -1
- package/dist/cjs/typed-routes/injectRouteHelpers.cjs +136 -0
- package/dist/cjs/typed-routes/injectRouteHelpers.js +104 -0
- package/dist/cjs/typed-routes/injectRouteHelpers.js.map +6 -0
- package/dist/cjs/typed-routes/injectRouteHelpers.native.js +146 -0
- package/dist/cjs/typed-routes/injectRouteHelpers.native.js.map +1 -0
- package/dist/cjs/vite/loadConfig.cjs +18 -12
- package/dist/cjs/vite/loadConfig.js +20 -13
- package/dist/cjs/vite/loadConfig.js.map +1 -1
- package/dist/cjs/vite/loadConfig.native.js +18 -11
- package/dist/cjs/vite/loadConfig.native.js.map +1 -1
- package/dist/cjs/vite/plugins/generateFileSystemRouteTypesPlugin.cjs +3 -2
- package/dist/cjs/vite/plugins/generateFileSystemRouteTypesPlugin.js +13 -3
- package/dist/cjs/vite/plugins/generateFileSystemRouteTypesPlugin.js.map +1 -1
- package/dist/cjs/vite/plugins/generateFileSystemRouteTypesPlugin.native.js +9 -6
- package/dist/cjs/vite/plugins/generateFileSystemRouteTypesPlugin.native.js.map +1 -1
- package/dist/esm/cli/generateRoutes.js +19 -2
- package/dist/esm/cli/generateRoutes.js.map +1 -1
- package/dist/esm/cli/generateRoutes.mjs +19 -4
- package/dist/esm/cli/generateRoutes.mjs.map +1 -1
- package/dist/esm/cli/generateRoutes.native.js +29 -4
- package/dist/esm/cli/generateRoutes.native.js.map +1 -1
- package/dist/esm/cli.js +4 -0
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/cli.mjs +4 -0
- package/dist/esm/cli.mjs.map +1 -1
- package/dist/esm/cli.native.js +4 -0
- package/dist/esm/cli.native.js.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/router/createRoute.js +12 -1
- package/dist/esm/router/createRoute.js.map +1 -1
- package/dist/esm/router/createRoute.mjs +12 -1
- package/dist/esm/router/createRoute.mjs.map +1 -1
- package/dist/esm/router/createRoute.native.js +13 -2
- package/dist/esm/router/createRoute.native.js.map +1 -1
- package/dist/esm/typed-routes/generateRouteTypes.js +14 -3
- package/dist/esm/typed-routes/generateRouteTypes.js.map +1 -1
- package/dist/esm/typed-routes/generateRouteTypes.mjs +14 -3
- package/dist/esm/typed-routes/generateRouteTypes.mjs.map +1 -1
- package/dist/esm/typed-routes/generateRouteTypes.native.js +29 -3
- package/dist/esm/typed-routes/generateRouteTypes.native.js.map +1 -1
- package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.js +38 -2
- package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.js.map +1 -1
- package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.mjs +39 -2
- package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.mjs.map +1 -1
- package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.native.js +40 -2
- package/dist/esm/typed-routes/getTypedRoutesDeclarationFile.native.js.map +1 -1
- package/dist/esm/typed-routes/injectRouteHelpers.js +89 -0
- package/dist/esm/typed-routes/injectRouteHelpers.js.map +6 -0
- package/dist/esm/typed-routes/injectRouteHelpers.mjs +113 -0
- package/dist/esm/typed-routes/injectRouteHelpers.mjs.map +1 -0
- package/dist/esm/typed-routes/injectRouteHelpers.native.js +120 -0
- package/dist/esm/typed-routes/injectRouteHelpers.native.js.map +1 -0
- package/dist/esm/vite/loadConfig.js +20 -13
- package/dist/esm/vite/loadConfig.js.map +1 -1
- package/dist/esm/vite/loadConfig.mjs +18 -12
- package/dist/esm/vite/loadConfig.mjs.map +1 -1
- package/dist/esm/vite/loadConfig.native.js +18 -11
- package/dist/esm/vite/loadConfig.native.js.map +1 -1
- package/dist/esm/vite/plugins/generateFileSystemRouteTypesPlugin.js +13 -3
- package/dist/esm/vite/plugins/generateFileSystemRouteTypesPlugin.js.map +1 -1
- package/dist/esm/vite/plugins/generateFileSystemRouteTypesPlugin.mjs +3 -2
- package/dist/esm/vite/plugins/generateFileSystemRouteTypesPlugin.mjs.map +1 -1
- package/dist/esm/vite/plugins/generateFileSystemRouteTypesPlugin.native.js +9 -6
- package/dist/esm/vite/plugins/generateFileSystemRouteTypesPlugin.native.js.map +1 -1
- package/package.json +10 -10
- package/src/cli/generateRoutes.ts +52 -4
- package/src/cli.ts +5 -0
- package/src/index.ts +13 -0
- package/src/interfaces/router.ts +19 -0
- package/src/router/createRoute.ts +16 -3
- package/src/typed-routes/generateRouteTypes.ts +46 -2
- package/src/typed-routes/getTypedRoutesDeclarationFile.ts +70 -0
- package/src/typed-routes/injectRouteHelpers.ts +186 -0
- package/src/vite/loadConfig.ts +29 -17
- package/src/vite/plugins/generateFileSystemRouteTypesPlugin.tsx +14 -3
- package/src/vite/types.ts +26 -0
- package/types/cli/generateRoutes.d.ts +1 -0
- package/types/cli/generateRoutes.d.ts.map +1 -1
- package/types/index.d.ts +13 -3
- package/types/index.d.ts.map +1 -1
- package/types/interfaces/router.d.ts +16 -0
- package/types/interfaces/router.d.ts.map +1 -1
- package/types/router/createRoute.d.ts +28 -13
- package/types/router/createRoute.d.ts.map +1 -1
- package/types/typed-routes/generateRouteTypes.d.ts +1 -1
- package/types/typed-routes/generateRouteTypes.d.ts.map +1 -1
- package/types/typed-routes/getTypedRoutesDeclarationFile.d.ts.map +1 -1
- package/types/typed-routes/injectRouteHelpers.d.ts +12 -0
- package/types/typed-routes/injectRouteHelpers.d.ts.map +1 -0
- package/types/utils/redirect.d.ts +1 -3
- package/types/utils/redirect.d.ts.map +1 -1
- package/types/vite/loadConfig.d.ts +1 -1
- package/types/vite/loadConfig.d.ts.map +1 -1
- package/types/vite/plugins/generateFileSystemRouteTypesPlugin.d.ts.map +1 -1
- package/types/vite/types.d.ts +25 -0
- 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
|
package/src/interfaces/router.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
+
}
|
package/src/vite/loadConfig.ts
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
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(
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateRoutes.d.ts","sourceRoot":"","sources":["../../src/cli/generateRoutes.ts"],"names":[],"mappings":"
|
|
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
|
|
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';
|
package/types/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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;
|