nuxt-typed-router 1.2.5 โ†’ 2.0.0-beta.1

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/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # ๐Ÿš—๐Ÿšฆ Typed Router for Nuxt
2
2
 
3
+ <p align="center">
4
+ <img width='100' src="https://raw.githubusercontent.com/victorgarciaesgi/nuxt-typed-router/master/.github/images/logo.png" alt="nuxt-typed-router logo">
5
+ </p>
6
+
7
+
3
8
  [npm-version-src]: https://img.shields.io/npm/v/nuxt-typed-router.svg
4
9
  [npm-version-href]: https://www.npmjs.com/package/nuxt-typed-router
5
10
  [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-typed-router.svg
@@ -9,9 +14,9 @@
9
14
  [![npm version][npm-version-src]][npm-version-href]
10
15
  [![npm downloads][npm-downloads-src]][npm-downloads-href]
11
16
  [![npm downloads][npm-total-downloads-src]][npm-downloads-href]
12
- <img src='https://img.shields.io/npm/l/simple-graphql-to-typescript.svg'>
17
+ <img src='https://img.shields.io/npm/l/nuxt-typed-router.svg'>
13
18
 
14
- > Provide a type safe router to Nuxt with auto-generated typed definitions for route names and autocompletion for route params
19
+ ## Provide a type safe router to Nuxt with auto-generated typed definitions for route names and autocompletion for route params
15
20
 
16
21
  - ๐ŸŽ Provides a hook `useTypedRouter` that returns an alias of `$typedRouter` and also a typed list of your routes
17
22
  - ๐Ÿšš Expose a global method `$typedRouter` (clone of vue-router), but typed with the routes defined in `pages` directory
@@ -28,7 +33,7 @@ Demo ๐Ÿงช : [nuxt-typed-router-demo](https://github.com/victorgarciaesgi/nuxt-ty
28
33
 
29
34
  <br/>
30
35
  <p align="center">
31
- <img src="https://github.com/victorgarciaesgi/nuxt-typed-router/blob/master/medias/in-action.gif?raw=true"/>
36
+ <img src="https://github.com/victorgarciaesgi/nuxt-typed-router/blob/master/.github/images/in-action.gif?raw=true"/>
32
37
  </p>
33
38
  <br/>
34
39
 
@@ -42,9 +47,9 @@ yarn add -D nuxt-typed-router
42
47
  npm install -D nuxt-typed-router
43
48
  ```
44
49
 
45
- ### For Nuxt 2
50
+ ### Nuxt 2 legacy
46
51
 
47
- For Nuxt 2 usage, check out the docs at the [`nuxt2` branch](https://github.com/victorgarciaesgi/nuxt-typed-router/tree/nuxt2)
52
+ Nuxt 2 version is no longer maintained, but still available in [`nuxt2` branch](https://github.com/victorgarciaesgi/nuxt-typed-router/tree/nuxt2)
48
53
 
49
54
  ```bash
50
55
  yarn add -D nuxt-typed-router@legacy
@@ -52,188 +57,24 @@ yarn add -D nuxt-typed-router@legacy
52
57
  npm install -D nuxt-typed-router@legacy
53
58
  ```
54
59
 
55
- # Configuration
60
+ # Quick start
56
61
 
57
62
  First, register the module in the `nuxt.config.ts`
58
63
 
59
64
  ```ts
60
65
  export default defineNuxtConfig({
61
66
  modules: ['nuxt-typed-router'],
62
- nuxtTypedRouter: {
63
- // options
64
- },
65
- });
66
- ```
67
-
68
- ## Options:
69
-
70
- ```ts
71
- interface ModuleOptions {
72
- /** Output directory where you cant the files to be saved
73
- * (ex: "./models")
74
- * @default "<srcDir>/generated"
75
- */
76
- outDir?: string;
77
- /** Name of the routesNames object (ex: "routesTree")
78
- * @default "routerPagesNames"
79
- * */
80
- routesObjectName?: string;
81
- }
82
- ```
83
-
84
- # Generated files
85
-
86
- The module will generate 4 files each time you modify the `pages` folder :
87
-
88
- - `~/<outDir>/__routes.ts` with the global object of the route names inside.
89
- - `~/<outDir>/__useTypedRouter.ts` Composable to simply access your typed routes
90
- - `~/<outDir>/typed-router.d.ts` containing the global typecript definitions and exports
91
- - `~/plugins/__typed_router.ts` Plugin that will inject `$typedRouter` and `$routesList` (`@nuxt/kit` has problems registering plugin templates so this is a workaround)
92
-
93
- # Usage in Vue/Nuxt
94
-
95
- <br/>
96
-
97
- ### **_Requirements_**
98
-
99
- You can specify the output dir of the generated files in your configuration. It defaults to `<srcDir>/generated`
100
-
101
- ```ts
102
- export default defineNuxtConfig({
103
- buildModules: ['nuxt-typed-router'],
104
- nuxtTypedRouter: {
105
- outDir: './generated',
106
- },
107
- });
108
- ```
109
-
110
- # How it works
111
-
112
- Given this structure
113
-
114
- โ”œโ”€โ”€ pages
115
- โ”œโ”€โ”€ index
116
- โ”œโ”€โ”€ content
117
- โ”œโ”€โ”€ [id].vue
118
- โ”œโ”€โ”€ content.vue
119
- โ”œโ”€โ”€ index.vue
120
- โ”œโ”€โ”€ communication.vue
121
- โ”œโ”€โ”€ statistics.vue
122
- โ”œโ”€โ”€ [user].vue
123
- โ”œโ”€โ”€ index.vue
124
- โ”œโ”€โ”€ forgotpassword.vue
125
- โ”œโ”€โ”€ reset-password.vue
126
- โ”‚ โ””โ”€โ”€ login.vue
127
- โ””โ”€โ”€ ...
128
-
129
- The generated route list will look like this
130
-
131
- ```ts
132
- export const routerPagesNames = {
133
- forgotpassword: 'forgotpassword' as const,
134
- login: 'login' as const,
135
- resetPassword: 'reset-password' as const,
136
- index: {
137
- index: 'index' as const,
138
- communication: 'index-communication' as const,
139
- content: {
140
- id: 'index-content-id' as const,
141
- },
142
- statistics: 'index-statistics' as const,
143
- user: 'index-user' as const,
144
- },
145
- };
146
- export type TypedRouteList =
147
- | 'forgotpassword'
148
- | 'login'
149
- | 'reset-password'
150
- | 'index'
151
- | 'index-communication'
152
- | 'index-content-id'
153
- | 'index-statistics'
154
- | 'index-user';
155
- ```
156
-
157
- > nuxt-typed-router will also create a plugin in your `<srcDir>/plugins` folder with the injected `$typedRouter` and `$routesList` helpers
158
-
159
- # Usage with `useTypedRouter` hook
160
-
161
- `useTypedRouter` is an exported composable from nuxt-typed-router. It contains a clone of `vue-router` but with strictly typed route names and params type-check
162
-
163
- ```vue
164
- <script lang="ts">
165
- // The path here is `~/generated` because I set `outDir: './generated'` in my module options
166
- import { useTypedRouter } from '~/generated';
167
-
168
- export default defineComponent({
169
- setup() {
170
- // Fully typed
171
- const { router, routes } = useTypedRouter();
172
-
173
- function navigate() {
174
- // Autocompletes the name and infer the params
175
- router.push({ name: routes.index.user, params: { user: 1 } }); // โœ… valid
176
- router.push({ name: routes.index.user, params: { foo: 1 } }); // โŒ invalid
177
- }
178
-
179
- return { navigate };
180
- },
181
67
  });
182
- </script>
183
68
  ```
184
69
 
185
- # Usage with `$typedRouter` and `$routesList` injected helpers
186
-
187
- `$typedRouter` is an injected clone of vue-router `$router`, but fully typed with all your routes.
188
- It's available anywhere you have access to Nuxt context
189
-
190
- ```vue
191
- <script lang="ts">
192
- import { defineComponent } from 'vue';
193
-
194
- export default defineComponent({
195
- name: 'Index',
196
- setup() {
197
- const { $typedRouter, $routesList } = useNuxtApp();
198
-
199
- function navigate() {
200
- $typedRouter.push({ name: $routesList.activate });
201
- }
202
-
203
- return {
204
- navigate,
205
- };
206
- },
207
- });
208
- </script>
209
- ```
210
-
211
- # Usage outside Vue component
212
-
213
- You can import the `useTypedRouter` composable from where it's generated.
214
- Exemple with `pinia` store here
215
-
216
- ```ts
217
- import pinia from 'pinia';
218
- import { useTypedRouter } from '~/generated';
219
-
220
- export const useFooStore = defineStore('foo', {
221
- actions: {
222
- bar() {
223
- const { router, routes } = useTypedRouter();
224
- router.push({ name: routes.index.user, params: { user: 2 } });
225
- },
226
- },
227
- });
228
- ```
229
70
 
230
71
  ## Development
231
72
 
232
73
  1. Clone this repository
233
- 2. Install dependencies using `yarn`
234
- 3. Build project for local tests `yarn build:local`
235
- 4. Start dev playground `yarn play`
236
- 5. Build project for deploy `yarn prepack`
74
+ 2. Install dependencies using `pnpm`
75
+ 3. Build project for local tests `pnpm build:local`
76
+ 4. Start dev playground `pnpm play`
77
+ 5. Build project for deploy `pnpm prepack`
237
78
 
238
79
  ## ๐Ÿ“‘ License
239
80
 
package/dist/module.d.ts CHANGED
@@ -1,17 +1,9 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
3
  interface ModuleOptions {
4
- /** Output directory where you cant the files to be saved (ex: "./generated")
5
- * @default "<srcDir>/generated"
6
- */
7
- outDir?: string;
8
- /** Name of the routesNames object (ex: "routesTree")
9
- * @default "routerPagesNames"
10
- * */
11
- routesObjectName?: string;
12
4
  /**
13
5
  * Set to false if you don't want a plugin generated
14
- * @default true
6
+ * @default false
15
7
  */
16
8
  plugin?: boolean;
17
9
  }
package/dist/module.json CHANGED
@@ -5,5 +5,5 @@
5
5
  "nuxt": "^3.0.0-rc.1",
6
6
  "bridge": false
7
7
  },
8
- "version": "1.2.5"
8
+ "version": "2.0.0-beta.1"
9
9
  }
package/dist/module.mjs CHANGED
@@ -1,13 +1,330 @@
1
+ import { fileURLToPath } from 'url';
1
2
  import { extendPages, defineNuxtModule } from '@nuxt/kit';
2
3
  import chalk from 'chalk';
3
4
  import logSymbols from 'log-symbols';
4
5
  import prettier from 'prettier';
5
6
  import fs from 'fs';
6
- import { fileURLToPath } from 'url';
7
7
  import { dirname, resolve } from 'pathe';
8
8
  import mkdirp from 'mkdirp';
9
9
  import { camelCase } from 'lodash-es';
10
10
 
11
+ const staticTypesImports = `
12
+ import type {
13
+ NavigationFailure,
14
+ RouteLocation,
15
+ RouteLocationNormalizedLoaded,
16
+ RouteLocationOptions,
17
+ RouteQueryAndHash,
18
+ RouteLocationRaw,
19
+ Router,
20
+ } from 'vue-router';
21
+ import type { DefineComponent } from 'vue';
22
+ import type { NuxtLinkProps } from '#app';
23
+ import type {
24
+ TypedRouteList,
25
+ TypedRouteNamedMapper,
26
+ TypedRouteParams,
27
+ ResolvedTypedRouteNamedMapper,
28
+ } from './__routes';
29
+ `;
30
+
31
+ const staticTypeUtils = `
32
+ // Type utils
33
+ type ExtractRequiredParameters<T extends Record<string, any>> = Pick<
34
+ T,
35
+ { [K in keyof T]: undefined extends T[K] ? never : K }[keyof T]
36
+ >;
37
+
38
+ type HasOneRequiredParameter<T extends TypedRouteList> = [TypedRouteParams[T]] extends [never]
39
+ ? false
40
+ : [keyof ExtractRequiredParameters<TypedRouteParams[T]>] extends [undefined]
41
+ ? false
42
+ : true;
43
+
44
+ type TypedLocationAsRelativeRaw<T extends TypedRouteList> = {
45
+ name?: T;
46
+ } & ([TypedRouteParams[T]] extends [never]
47
+ ? {}
48
+ : HasOneRequiredParameter<T> extends false
49
+ ? { params?: TypedRouteParams[T] }
50
+ : { params: TypedRouteParams[T] });
51
+
52
+ type ResolvedTypedLocationAsRelativeRaw<T extends TypedRouteList> = {
53
+ name?: T;
54
+ } & ([TypedRouteParams[T]] extends [never] ? {} : { params: Required<TypedRouteParams[T]> });
55
+
56
+ type TypedRouteLocationRaw = RouteQueryAndHash & TypedRouteNamedMapper & RouteLocationOptions;
57
+
58
+ type _TypedRoute = Omit<RouteLocationNormalizedLoaded, 'name' | 'params'> &
59
+ ResolvedTypedRouteNamedMapper;
60
+ type _TypedNamedRoute<T extends TypedRouteList> = Omit<
61
+ RouteLocationNormalizedLoaded,
62
+ 'name' | 'params'
63
+ > &
64
+ ResolvedTypedLocationAsRelativeRaw<T>;
65
+
66
+ /** Augmented Router interface */
67
+ interface _TypedRouter
68
+ extends Omit<Router, 'removeRoute' | 'hasRoute' | 'resolve' | 'push' | 'replace'> {
69
+ /**
70
+ * Remove an existing route by its name.
71
+ *
72
+ * @param name - Name of the route to remove
73
+ */
74
+ removeRoute(name: TypedRouteList): void;
75
+ /**
76
+ * Checks if a route with a given name exists
77
+ *
78
+ * @param name - Name of the route to check
79
+ */
80
+ hasRoute(name: TypedRouteList): boolean;
81
+ /**
82
+ * Returns the {@link RouteLocation | normalized version} of a
83
+ * {@link RouteLocationRaw | route location}. Also includes an \`href\` property
84
+ * that includes any existing \`base\`. By default the \`currentLocation\` used is
85
+ * \`route.currentRoute\` and should only be overriden in advanced use cases.
86
+ *
87
+ * @param to - Raw route location to resolve
88
+ * @param currentLocation - Optional current location to resolve against
89
+ */
90
+ resolve(
91
+ to: TypedRouteLocationRaw,
92
+ currentLocation?: RouteLocationNormalizedLoaded
93
+ ): RouteLocation & {
94
+ href: string;
95
+ };
96
+ /**
97
+ * Programmatically navigate to a new URL by pushing an entry in the history
98
+ * stack.
99
+ *
100
+ * @param to - Route location to navigate to
101
+ */
102
+ push(to: TypedRouteLocationRaw): Promise<NavigationFailure | void | undefined>;
103
+ /**
104
+ * Programmatically navigate to a new URL by replacing the current entry in
105
+ * the history stack.
106
+ *
107
+ * @param to - Route location to navigate to
108
+ */
109
+ replace(to: TypedRouteLocationRaw): Promise<NavigationFailure | void | undefined>;
110
+ }
111
+
112
+ export interface TypedRouter extends _TypedRouter {}
113
+ export type TypedRoute = _TypedRoute;
114
+ export type TypedNamedRoute<T extends TypedRouteList> = _TypedNamedRoute<T>;
115
+
116
+ declare global {
117
+ export interface TypedRouter extends _TypedRouter {}
118
+ export type TypedRoute = _TypedRoute;
119
+ export type TypedNamedRoute<T extends TypedRouteList> = _TypedNamedRoute<T>;
120
+ }
121
+
122
+ type TypedNuxtLinkProps = Omit<NuxtLinkProps, 'to'> & {
123
+ to: string | Omit<Exclude<RouteLocationRaw, string>, 'name'> & TypedRouteNamedMapper;
124
+ };
125
+
126
+ type _NuxtLink = DefineComponent<
127
+ TypedNuxtLinkProps,
128
+ {},
129
+ {},
130
+ import('vue').ComputedOptions,
131
+ import('vue').MethodOptions,
132
+ import('vue').ComponentOptionsMixin,
133
+ import('vue').ComponentOptionsMixin,
134
+ {},
135
+ string,
136
+ import('vue').VNodeProps &
137
+ import('vue').AllowedComponentProps &
138
+ import('vue').ComponentCustomProps,
139
+ Readonly<TypedNuxtLinkProps>,
140
+ {}
141
+ >;
142
+
143
+ declare module '@vue/runtime-core' {
144
+ export interface GlobalComponents {
145
+ NuxtLink: _NuxtLink;
146
+ }
147
+ }
148
+ `;
149
+
150
+ const watermarkTemplate = `
151
+ // @ts-nocheck
152
+ // eslint-disable
153
+ /**
154
+ * ---------------------------------------------------
155
+ * \u{1F697}\u{1F6A6} Generated by nuxt-typed-router. Do not modify !
156
+ * ---------------------------------------------------
157
+ * */
158
+
159
+ `;
160
+
161
+ function createDeclarationRoutesFile() {
162
+ return `
163
+ ${watermarkTemplate}
164
+
165
+ ${staticTypesImports}
166
+
167
+ ${staticTypeUtils}
168
+ `;
169
+ }
170
+
171
+ function createRuntimeIndexFile() {
172
+ return `
173
+ ${watermarkTemplate}
174
+ export {routesNames, TypedRouteList} from './__routes';
175
+ export * from './__useTypedRouter';
176
+ export * from './__useTypedRoute';
177
+ `;
178
+ }
179
+
180
+ function createRuntimePluginFile(routesDeclTemplate) {
181
+ return `
182
+ ${watermarkTemplate}
183
+ import { defineNuxtPlugin } from '#app';
184
+
185
+ export default defineNuxtPlugin(() => {
186
+ const router = useRouter();
187
+ const route = useRoute();
188
+ const routesNames = ${routesDeclTemplate};
189
+
190
+ return {
191
+ provide: {
192
+ typedRouter: router as TypedRouter,
193
+ typedRoute: route as TypedRoute,
194
+ routesNames,
195
+ },
196
+ };
197
+ });
198
+ `;
199
+ }
200
+
201
+ function createTypedRouteListExport(routesList) {
202
+ return `export type TypedRouteList = ${routesList.map((m) => `'${m}'`).join("|\n")}`;
203
+ }
204
+ function createTypedRouteParamsExport(routesParams) {
205
+ return `export type TypedRouteParams = {
206
+ ${routesParams.map(
207
+ ({ name, params }) => `"${name}": ${params.length ? `{
208
+ ${params.map(({ key, required, type }) => `"${key}"${required ? "" : "?"}: ${type}`).join(",\n")}
209
+ }` : "never"}`
210
+ ).join(",\n")}
211
+ }`;
212
+ }
213
+ function createTypedRouteNamedMapperExport(routesParams) {
214
+ return `export type TypedRouteNamedMapper =
215
+ ${routesParams.map(
216
+ ({ name, params }) => `{name: "${name}" ${params.length ? `, params${params.some((s) => s.required) ? "" : "?"}: {
217
+ ${params.map(({ key, required, type }) => `"${key}"${required ? "" : "?"}: ${type}`).join(",\n")}
218
+ }` : ""}}`
219
+ ).join("|\n")}
220
+ `;
221
+ }
222
+ function createResolvedTypedRouteNamedMapperExport(routesParams) {
223
+ return `export type ResolvedTypedRouteNamedMapper =
224
+ {
225
+ name: TypedRouteList;
226
+ params: unknown;
227
+ } & (
228
+ ${routesParams.map(
229
+ ({ name, params }) => `{name: "${name}" ${params.length ? `, params: {
230
+ ${params.map(({ key, type }) => `"${key}": ${type}`).join(",\n")}
231
+ }` : ""}}`
232
+ ).join("|\n")}
233
+ )
234
+ `;
235
+ }
236
+
237
+ function createRuntimeRoutesFile({
238
+ routesList,
239
+ routesObjectTemplate,
240
+ routesDeclTemplate,
241
+ routesParams
242
+ }) {
243
+ return `
244
+ ${watermarkTemplate}
245
+
246
+ export const routesNames = ${routesObjectTemplate};
247
+
248
+ ${createTypedRouteListExport(routesList)}
249
+
250
+ export type RouteListDecl = ${routesDeclTemplate};
251
+
252
+ /**
253
+ * Routes params are only required for the exact targeted route name,
254
+ * vue-router behaviour allow to navigate between children routes without the need to provide all the params every time.
255
+ * So we can't enforce params when navigating between routes, only a \`[xxx].vue\` page will have required params in the type definition
256
+ *
257
+ *
258
+ * */
259
+
260
+ ${createTypedRouteParamsExport(routesParams)}
261
+
262
+ ${createTypedRouteNamedMapperExport(routesParams)}
263
+
264
+ ${createResolvedTypedRouteNamedMapperExport(routesParams)}
265
+ `;
266
+ }
267
+
268
+ function createUseTypedRouteFile(routesDeclTemplate) {
269
+ return `
270
+ ${watermarkTemplate}
271
+ import { useRoute } from '#app';
272
+ import { TypedRouteList } from './__routes';
273
+
274
+ /** Acts the same as \`useRoute\`, but typed.
275
+ *
276
+ * @exemple
277
+ *
278
+ * \`\`\`ts
279
+ * const route = useTypedRoute();
280
+ * \`\`\`
281
+ *
282
+ * \`\`\`ts
283
+ * const route = useTypedRoute('my-route-with-param-id');
284
+ * route.params.id // autocompletes!
285
+ * \`\`\`
286
+ *
287
+ * \`\`\`ts
288
+ * const route = useTypedRoute();
289
+ * if (route.name === 'my-route-with-param-id') {
290
+ * route.params.id // autocompletes!
291
+ * }
292
+ * \`\`\`
293
+ */
294
+ export function useTypedRoute<T extends TypedRouteList = never>(
295
+ name?: T
296
+ ): [T] extends [never] ? TypedRoute : TypedNamedRoute<T> {
297
+ const route = useRoute();
298
+
299
+ return route as any;
300
+ }
301
+
302
+ `;
303
+ }
304
+
305
+ function createRuntimeUseTypedRouterFile(routesDeclTemplate) {
306
+ return `
307
+ ${watermarkTemplate}
308
+ import { useRouter } from '#app';
309
+ import { TypedRouter } from './typed-router';
310
+
311
+ /** Returns instances of $typedRouter and $routesList fully typed to use in your components or your Vuex/Pinia store
312
+ *
313
+ * @exemple
314
+ *
315
+ * \`\`\`ts
316
+ * const { router, routes } = useTypedRouter();
317
+ * \`\`\`
318
+ */
319
+ export function useTypedRouter(): TypedRouter {
320
+ const router = useRouter();
321
+
322
+ return router;
323
+ };
324
+
325
+ `;
326
+ }
327
+
11
328
  const { resolveConfig, format } = prettier;
12
329
  const defaultPrettierOptions = {
13
330
  printWidth: 100,
@@ -36,9 +353,15 @@ async function formatOutputWithPrettier(template) {
36
353
  }
37
354
 
38
355
  dirname(fileURLToPath(import.meta.url));
39
- async function saveRouteFiles(outDir, srcDir, fileName, content) {
356
+ async function processPathAndWriteFile({
357
+ content,
358
+ fileName,
359
+ srcDir,
360
+ outDir
361
+ }) {
40
362
  try {
41
- const processedOutDir = resolve(srcDir, outDir);
363
+ const finalOutDir = outDir ?? `.nuxt/typed-router`;
364
+ const processedOutDir = resolve(srcDir, finalOutDir);
42
365
  const outputFile = resolve(process.cwd(), `${processedOutDir}/${fileName}`);
43
366
  const formatedContent = await formatOutputWithPrettier(content);
44
367
  if (fs.existsSync(outputFile)) {
@@ -63,6 +386,100 @@ async function writeFile(path, content) {
63
386
  }
64
387
  }
65
388
 
389
+ function handlePluginFileSave({
390
+ nuxt,
391
+ srcDir,
392
+ routesDeclTemplate
393
+ }) {
394
+ const pluginName = "__typed-router.ts";
395
+ nuxt.hook("build:done", async () => {
396
+ const pluginFolder = `${srcDir}/plugins`;
397
+ await processPathAndWriteFile({
398
+ outDir: pluginFolder,
399
+ srcDir,
400
+ fileName: pluginName,
401
+ content: createRuntimePluginFile(routesDeclTemplate)
402
+ });
403
+ });
404
+ }
405
+
406
+ let previousGeneratedRoutes = "";
407
+ async function saveGeneratedFiles({
408
+ srcDir,
409
+ outputData: { routesDeclTemplate, routesList, routesObjectTemplate, routesParams }
410
+ }) {
411
+ const filesMap = [
412
+ {
413
+ fileName: "__useTypedRouter.ts",
414
+ content: createRuntimeUseTypedRouterFile()
415
+ },
416
+ {
417
+ fileName: "__useTypedRoute.ts",
418
+ content: createUseTypedRouteFile()
419
+ },
420
+ {
421
+ fileName: `__routes.ts`,
422
+ content: createRuntimeRoutesFile({
423
+ routesList,
424
+ routesObjectTemplate,
425
+ routesDeclTemplate,
426
+ routesParams
427
+ })
428
+ },
429
+ {
430
+ fileName: `typed-router.d.ts`,
431
+ content: createDeclarationRoutesFile()
432
+ },
433
+ {
434
+ fileName: "index.ts",
435
+ content: createRuntimeIndexFile()
436
+ }
437
+ ];
438
+ await Promise.all(
439
+ filesMap.map(({ content, fileName }) => processPathAndWriteFile({ srcDir, content, fileName }))
440
+ );
441
+ if (previousGeneratedRoutes !== routesList.join(",")) {
442
+ previousGeneratedRoutes = routesList.join(",");
443
+ console.log(logSymbols.success, `[typed-router] Routes definitions generated`);
444
+ }
445
+ }
446
+
447
+ function isItemLast(array, index) {
448
+ return array ? index === array.length - 1 : false;
449
+ }
450
+
451
+ const routeParamExtractRegxp = /(:(\w+)(\?)?)+/g;
452
+ function extractRouteParamsFromPath(path, isIndexFileForRouting, previousParams) {
453
+ let params = [];
454
+ let matches;
455
+ do {
456
+ matches = routeParamExtractRegxp.exec(path);
457
+ if (matches) {
458
+ const [_, mtch, key, optional] = matches;
459
+ if (mtch) {
460
+ params.push({ name: key, required: !optional });
461
+ }
462
+ }
463
+ } while (matches);
464
+ let allMergedParams = params.map(
465
+ ({ name, required }) => ({
466
+ key: name,
467
+ type: "string | number",
468
+ required
469
+ })
470
+ );
471
+ if (previousParams?.length) {
472
+ allMergedParams = previousParams.map((m) => ({ ...m, required: false })).concat(allMergedParams);
473
+ }
474
+ if (!params.length && isIndexFileForRouting) {
475
+ const lastItem = allMergedParams[allMergedParams.length - 1];
476
+ if (lastItem) {
477
+ lastItem.required = true;
478
+ }
479
+ }
480
+ return allMergedParams;
481
+ }
482
+
66
483
  function extractMatchingSiblings(mainRoute, siblingRoutes) {
67
484
  return siblingRoutes?.filter((s) => {
68
485
  const chunkName = extractChunkMain(mainRoute.file);
@@ -92,61 +509,6 @@ function extractChunkMain(chunkName) {
92
509
  return chunkArray?.join("/");
93
510
  }
94
511
 
95
- const routeParamExtractRegxp = /:(\w+)/;
96
- function extractRouteParamsFromPath(path, previousParams) {
97
- const params = path.match(routeParamExtractRegxp) ?? [];
98
- params?.shift();
99
- let allMergedParams = params.map(
100
- (m) => ({
101
- key: m,
102
- type: "string | number",
103
- required: true
104
- })
105
- );
106
- if (previousParams?.length) {
107
- allMergedParams = allMergedParams.concat(
108
- previousParams.map((m) => ({ ...m, required: false }))
109
- );
110
- }
111
- return allMergedParams;
112
- }
113
-
114
- function isItemLast(array, index) {
115
- return index === array.length - 1;
116
- }
117
- function constructRouteMap(routesConfig) {
118
- try {
119
- let routesObjectTemplate = "{";
120
- let routesDeclTemplate = "{";
121
- let routesList = [];
122
- let routesParams = [];
123
- const output = { routesObjectTemplate, routesDeclTemplate, routesList, routesParams };
124
- startGeneratorProcedure({
125
- output,
126
- routesConfig
127
- });
128
- return output;
129
- } catch (e) {
130
- throw new Error("Generation failed");
131
- }
132
- }
133
- function startGeneratorProcedure({
134
- output,
135
- routesConfig
136
- }) {
137
- routesConfig.forEach((route, index) => {
138
- const rootSiblingsRoutes = routesConfig.filter((rt) => rt.chunkName !== route.chunkName);
139
- walkThoughRoutes({
140
- route,
141
- level: 0,
142
- output,
143
- siblings: rootSiblingsRoutes,
144
- isLast: isItemLast(routesConfig, index)
145
- });
146
- });
147
- output.routesObjectTemplate += "}";
148
- output.routesDeclTemplate += "}";
149
- }
150
512
  function walkThoughRoutes({
151
513
  route,
152
514
  level,
@@ -168,7 +530,7 @@ function walkThoughRoutes({
168
530
  const nameKey = camelCase(parentPath || "index");
169
531
  output.routesObjectTemplate += `${nameKey}:{`;
170
532
  output.routesDeclTemplate += `"${nameKey}":{`;
171
- const allRouteParams = extractRouteParamsFromPath(route.path, previousParams);
533
+ const allRouteParams = extractRouteParamsFromPath(route.path, false, previousParams);
172
534
  childrenChunks?.map(
173
535
  (routeConfig, index) => walkThoughRoutes({
174
536
  route: routeConfig,
@@ -193,7 +555,12 @@ function walkThoughRoutes({
193
555
  output.routesObjectTemplate += `'${keyName}': '${route.name}' as const,`;
194
556
  output.routesDeclTemplate += `"${keyName}": "${route.name}"${isLast ? "" : ","}`;
195
557
  output.routesList.push(route.name);
196
- const allRouteParams = extractRouteParamsFromPath(route.path, previousParams);
558
+ const isIndexFileForRouting = route.path === "";
559
+ const allRouteParams = extractRouteParamsFromPath(
560
+ route.path,
561
+ isIndexFileForRouting,
562
+ previousParams
563
+ );
197
564
  output.routesParams.push({
198
565
  name: route.name,
199
566
  params: allRouteParams
@@ -201,229 +568,49 @@ function walkThoughRoutes({
201
568
  }
202
569
  }
203
570
 
204
- const signatureTemplate = `/**
205
- * ---------------------
206
- * \u{1F697}\u{1F6A6} Generated by nuxt-typed-router. Do not modify !
207
- * ---------------------
208
- * */
209
-
210
- `;
211
- const staticDeclImports = `
212
- import type {
213
- NavigationFailure,
214
- RouteLocation,
215
- RouteLocationNormalizedLoaded,
216
- RouteLocationOptions,
217
- RouteQueryAndHash,
218
- } from 'vue-router';
219
- import type { TypedRouteList } from './__routes'
220
- `;
221
- const staticDeclarations = `
222
- type TypedRouteParamsStructure = {
223
- [K in TypedRouteList]: Record<string, string | number> | never;
224
- };
225
-
226
- type TypedLocationAsRelativeRaw<T extends TypedRouteList> = {
227
- name?: T;
228
- params?: TypedRouteParams[T];
229
- };
230
-
231
- type TypedRouteLocationRaw<T extends TypedRouteList> = RouteQueryAndHash &
232
- TypedLocationAsRelativeRaw<T> &
233
- RouteLocationOptions;
234
-
235
- interface _TypedRouter {
236
- /**
237
- * Remove an existing route by its name.
238
- *
239
- * @param name - Name of the route to remove
240
- */
241
- removeRoute(name: TypedRouteList): void;
242
- /**
243
- * Checks if a route with a given name exists
244
- *
245
- * @param name - Name of the route to check
246
- */
247
- hasRoute(name: TypedRouteList): boolean;
248
- /**
249
- * Returns the {@link RouteLocation | normalized version} of a
250
- * {@link RouteLocationRaw | route location}. Also includes an \`href\` property
251
- * that includes any existing \`base\`. By default the \`currentLocation\` used is
252
- * \`route.currentRoute\` and should only be overriden in advanced use cases.
253
- *
254
- * @param to - Raw route location to resolve
255
- * @param currentLocation - Optional current location to resolve against
256
- */
257
- resolve<T extends TypedRouteList>(
258
- to: TypedRouteLocationRaw<T>,
259
- currentLocation?: RouteLocationNormalizedLoaded
260
- ): RouteLocation & {
261
- href: string;
262
- };
263
- /**
264
- * Programmatically navigate to a new URL by pushing an entry in the history
265
- * stack.
266
- *
267
- * @param to - Route location to navigate to
268
- */
269
- push<T extends TypedRouteList>(
270
- to: TypedRouteLocationRaw<T>
271
- ): Promise<NavigationFailure | void | undefined>;
272
- /**
273
- * Programmatically navigate to a new URL by replacing the current entry in
274
- * the history stack.
275
- *
276
- * @param to - Route location to navigate to
277
- */
278
- replace<T extends TypedRouteList>(
279
- to: TypedRouteLocationRaw<T>
280
- ): Promise<NavigationFailure | void | undefined>;
281
- }
282
-
283
- export interface TypedRouter extends _TypedRouter {}
284
- declare global {
285
- export interface TypedRouter extends _TypedRouter {}
571
+ function constructRouteMap(routesConfig) {
572
+ try {
573
+ let routesObjectTemplate = "{";
574
+ let routesDeclTemplate = "{";
575
+ let routesList = [];
576
+ let routesParams = [];
577
+ const output = { routesObjectTemplate, routesDeclTemplate, routesList, routesParams };
578
+ startGenerator({
579
+ output,
580
+ routesConfig
581
+ });
582
+ return output;
583
+ } catch (e) {
584
+ throw new Error("Generation failed");
286
585
  }
287
- `;
288
-
289
- function createRuntimePluginFile(routesDeclTemplate) {
290
- return `
291
- ${signatureTemplate}
292
- import { defineNuxtPlugin } from '#app';
293
-
294
- export default defineNuxtPlugin(() => {
295
- const router = useRouter();
296
- const routesList = ${routesDeclTemplate};
297
-
298
- return {
299
- provide: {
300
- typedRouter: router as TypedRouter,
301
- routesList,
302
- },
303
- };
304
- });
305
- `;
306
586
  }
307
- function createRuntimeHookFile(routesDeclTemplate) {
308
- return `
309
- ${signatureTemplate}
310
- import { useNuxtApp } from '#app';
311
- import { TypedRouter, RouteListDecl } from './typed-router';
312
-
313
- /** Returns instances of $typedRouter and $routesList fully typed to use in your components or your Vuex/Pinia store
314
- *
315
- * @exemple
316
- *
317
- * \`\`\`ts
318
- * const { router, routes } = useTypedRouter();
319
- * \`\`\`
320
- */
321
- export const useTypedRouter = (): {
322
- /** Export of $router with type check */
323
- router: TypedRouter,
324
- /** Contains a typed dictionnary of all your route names (for syntax sugar) */
325
- routes: RouteListDecl
326
- } => {
327
- const { $router } = useNuxtApp();
328
-
329
- const routesList = ${routesDeclTemplate};
330
-
331
- return {
332
- router: $router,
333
- routes: routesList,
334
- } as any;
335
- };
336
-
337
- `;
338
- }
339
- function createRuntimeIndexFile() {
340
- return `
341
- ${signatureTemplate}
342
- export * from './__routes';
343
- export * from './__useTypedRouter';
344
- `;
345
- }
346
- function createRuntimeRoutesFile({
347
- routesList,
348
- routesObjectTemplate,
349
- routesObjectName
350
- }) {
351
- return `
352
- ${signatureTemplate}
353
-
354
- export const ${routesObjectName} = ${routesObjectTemplate};
355
-
356
- ${createTypedRouteListExport(routesList)}
357
- `;
358
- }
359
- function createDeclarationRoutesFile({
360
- routesDeclTemplate,
361
- routesList,
362
- routesParams
363
- }) {
364
- return `
365
- ${signatureTemplate}
366
- ${staticDeclImports}
367
-
368
- export type RouteListDecl = ${routesDeclTemplate};
369
-
370
- ${createTypedRouteParamsExport(routesParams)}
371
-
372
- ${staticDeclarations}
373
- `;
374
- }
375
- function createTypedRouteListExport(routesList) {
376
- return `export type TypedRouteList = ${routesList.map((m) => `'${m}'`).join("|\n")}`;
377
- }
378
- function createTypedRouteParamsExport(routesParams) {
379
- return `export type TypedRouteParams = {
380
- ${routesParams.map(
381
- ({ name, params }) => `"${name}": ${params.length ? `{
382
- ${params.map(({ key, required, type }) => `"${key}"${required ? "" : "?"}: ${type}`).join(",\n")}
383
- }` : "never"}`
384
- ).join(",\n")}
385
- }`;
587
+ function startGenerator({ output, routesConfig }) {
588
+ routesConfig.forEach((route, index) => {
589
+ const rootSiblingsRoutes = routesConfig.filter((rt) => rt.chunkName !== route.chunkName);
590
+ walkThoughRoutes({
591
+ route,
592
+ level: 0,
593
+ output,
594
+ siblings: rootSiblingsRoutes,
595
+ isLast: isItemLast(routesConfig, index)
596
+ });
597
+ });
598
+ output.routesObjectTemplate += "}";
599
+ output.routesDeclTemplate += "}";
386
600
  }
387
601
 
388
- function routeHook({ outDir, plugin, routesObjectName }, srcDir, nuxt) {
602
+ function createTypedRouter({ srcDir, plugin, nuxt }) {
389
603
  try {
390
604
  extendPages(async (routes) => {
391
605
  if (routes.length) {
392
- const { routesDeclTemplate, routesList, routesObjectTemplate, routesParams } = constructRouteMap(routes);
606
+ const outputData = constructRouteMap(routes);
393
607
  if (plugin) {
394
- const pluginName = "__typed-router.ts";
395
- nuxt.hook("build:done", async () => {
396
- const pluginFolder = `${srcDir}/plugins`;
397
- await saveRouteFiles(
398
- pluginFolder,
399
- srcDir,
400
- pluginName,
401
- createRuntimePluginFile(routesDeclTemplate)
402
- );
403
- });
608
+ handlePluginFileSave({ nuxt, routesDeclTemplate: outputData.routesDeclTemplate, srcDir });
404
609
  }
405
- await Promise.all([
406
- saveRouteFiles(
407
- outDir,
408
- srcDir,
409
- "__useTypedRouter.ts",
410
- createRuntimeHookFile(routesDeclTemplate)
411
- ),
412
- saveRouteFiles(
413
- outDir,
414
- srcDir,
415
- `__routes.ts`,
416
- createRuntimeRoutesFile({ routesList, routesObjectTemplate, routesObjectName })
417
- ),
418
- saveRouteFiles(
419
- outDir,
420
- srcDir,
421
- `typed-router.d.ts`,
422
- createDeclarationRoutesFile({ routesDeclTemplate, routesList, routesParams })
423
- ),
424
- saveRouteFiles(outDir, srcDir, "index.ts", createRuntimeIndexFile())
425
- ]);
426
- console.log(logSymbols.success, `[typed-router] Routes definitions generated`);
610
+ await saveGeneratedFiles({
611
+ srcDir,
612
+ outputData
613
+ });
427
614
  } else {
428
615
  console.log(
429
616
  logSymbols.warning,
@@ -447,13 +634,19 @@ const module = defineNuxtModule({
447
634
  compatibility: { nuxt: "^3.0.0-rc.1", bridge: false }
448
635
  },
449
636
  defaults: {
450
- outDir: `./generated`,
451
- routesObjectName: "routerPagesNames"
637
+ plugin: false
452
638
  },
453
639
  setup(moduleOptions, nuxt) {
454
640
  const srcDir = nuxt.options.srcDir;
455
- const { plugin = true, ...otherOptions } = moduleOptions;
456
- nuxt.hook("pages:extend", () => routeHook({ ...otherOptions, plugin }, srcDir, nuxt));
641
+ const { plugin } = moduleOptions;
642
+ nuxt.options.alias = {
643
+ ...nuxt.options.alias,
644
+ "@typed-router": fileURLToPath(
645
+ new URL(`${nuxt.options.rootDir}/.nuxt/typed-router`, import.meta.url)
646
+ )
647
+ };
648
+ nuxt.hook("pages:extend", () => createTypedRouter({ srcDir, nuxt, plugin }));
649
+ createTypedRouter({ srcDir, nuxt, plugin });
457
650
  }
458
651
  });
459
652
 
package/main.d.ts CHANGED
@@ -1,2 +1 @@
1
- import './dist/types';
2
1
  export { default } from './dist/module';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-typed-router",
3
- "version": "1.2.5",
3
+ "version": "2.0.0-beta.1",
4
4
  "description": "Provide autocompletion for pages route names generated by Nuxt router",
5
5
  "type": "module",
6
6
  "main": "./dist/module.cjs",
@@ -20,9 +20,11 @@
20
20
  "dev": "nuxi dev playground",
21
21
  "dev:build": "nuxi build playground",
22
22
  "dev:prepare": "nuxt-module-build --stub && nuxi prepare playground",
23
- "build:test": "cross-env NUXT_BUILD_TYPE=stub yarn prepack && yarn dev:build",
24
- "test": "yarn dev:prepare && yarn build:test && vitest run",
25
- "test:watch": "yarn build:test && vitest"
23
+ "build:test": "cross-env NUXT_BUILD_TYPE=stub pnpm run prepack && pnpm run dev:build",
24
+ "test": "pnpm run dev:prepare && pnpm run build:test && vitest run",
25
+ "test:watch": "pnpm run build:test && vitest",
26
+ "docs:dev": "cd docs && pnpm run dev",
27
+ "docs:buid": "(cd docs && nuxi build)"
26
28
  },
27
29
  "publishConfig": {
28
30
  "access": "public"
@@ -67,11 +69,11 @@
67
69
  "@types/node": "^17.0.23",
68
70
  "@types/prettier": "^2.7.2",
69
71
  "cross-env": "^7.0.3",
70
- "eslint": "8.30.0",
71
- "eslint-config-prettier": "^8.5.0",
72
+ "eslint": "8.31.0",
73
+ "eslint-config-prettier": "^8.6.0",
72
74
  "nuxt": "3.0.0",
73
75
  "typescript": "^4.9.4",
74
- "vitest": "^0.26.1",
76
+ "vitest": "^0.27.0",
75
77
  "vue-router": "^4.1.6"
76
78
  }
77
79
  }