Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

callback to allow manipulating generated routes #43

Closed
danielroe opened this issue Aug 1, 2022 · 15 comments
Closed

callback to allow manipulating generated routes #43

danielroe opened this issue Aug 1, 2022 · 15 comments
Labels
💬 discussion topic that requires further discussion

Comments

@danielroe
Copy link
Collaborator

would be nice to pass a custom function to the unplugin that receives the scanned routes and allows manipulation, e.g. creating new routes, removing or modifying existing ones.

@posva
Copy link
Owner

posva commented Aug 1, 2022

Do you want access to the addPage, updatePage, removePage functions that exists in context.ts? What kind of manipulations do you need to do? Is it to create routes based on files located else where or are they going to be non readable files? Since right now, these functions read from the file, they would need some changes if this isn't enough

Edit: this is already possible with extendRoute() and beforeWriteFiles(). This issue is left open to gather feedback

@jods4
Copy link

jods4 commented Sep 3, 2022

Currently we use vite-plugin-pages. It has callbacks to manipulate individual routes and the complete route table.
We use both:

Per-route callback

We use it to simplify our <route> syntax to the max, kind of a micro-DSL (we have hundreds of routes).

For example we put common well-known metadata at the top level and it's moved to the meta field by the callback.

<route lang="yaml">
  title: Open tickets
  auth: Operators
</route>

Another example is defining some macros, where a simple property or value is expanded/transformed into a more complex meta structure.

This is just about convenience, of course we could write the meta block instead but with 100s of pages streamlining is really nice.

Route table callback

We use this to remove routes at build-time.

Our source code is actually deployed as multiple distinct applications that have some shared, and some app-specific modules.
We develop it as a big application that has everything (incl. some modules for dev only) but then at build time the final routing table only contains the pages specific to the application variant being built.

So our pages look like this:

<route lang="yaml">
  title: Invoices
  app: Accounting
</route>

The routes callback drops pages whose app attribute doesn't match the application being built.
Removing these pages from the routing table actually removes them from compilation entirely, as they're not referenced from anywhere else.

@posva
Copy link
Owner

posva commented Sep 4, 2022

For example we put common well-known metadata at the top level and it's moved to the meta field by the callback.

Do you parse this yourselves in a custom plugin?

Right now you can extend routes at runtime with https://github.com/posva/unplugin-vue-router#extendroutes but this one should be built time. It wouldn't receive an array though, it would receive a Tree, so a bit low level I think but at the same time it reflects much better the routing structure.

I think that with both functions, it should cover both cases. In the end, having the callback to modify just one route is convenience or more than that?

The routes callback drops pages whose app attribute doesn't match the application being built.
Removing these pages from the routing table actually removes them from compilation entirely, as they're not referenced from anywhere else.

Real nice!

@jods4
Copy link

jods4 commented Sep 4, 2022

Do you parse this yourselves in a custom plugin?

All the parsing is done by vite-plugin-pages.
Option extendroute accepts a function that takes the route being processed (as a js object) and its parent route (if any). It should return a modified route.

In the end, having the callback to modify just one route is convenience or more than that?

I don't see a situation where there's something you couldn't do by iterating a modifying the complete route table.
But clearly, it's very convenient to have a simple callback that takes one route.

It also does less work in case of HMR. You only get called for the routes that have been modified, whereas otherwise you'd have to go through the whole routing table (and you wouldn't know which routes are already modified and which aren't...).

@whttigress
Copy link

I have a different use case that is similar. I use the vite-plugin-pages onRoutesGenerated build time function that uses the routes array to parse the actual files and generate metadata for the routes and generate some index files. I think most of the other functionality that I use in their extendRoute build option I could move to the extendRoutes runtime option, but I do need some sort of build time function on the list of routes before I can convert over.

@andrewcourtice
Copy link

One of the use-cases we have for using a build-time extendRoute function is to generate basic search index information. Using the extendRoute method of vite-plugin-pages we can read the vue or markdown file (via vite-plugin-md) and strip away anything other than plain text in order to generate a search index.

It would be super handy to have build-time access to the generated routes (or each individual route) for this use-case.

@posva
Copy link
Owner

posva commented Feb 15, 2023

I published a new version with an experimental extendRoute() and beforeWriteFiles(). They both expose a wrapper around the route nodes that allow a limited set of operations.

I think that to make this better, it would be useful to have examples of your current extendRoute() and onRoutesGenerated() so I can help with the migration and add any missing features.

@andrewcourtice
Copy link

Thanks @posva! You're a legend!

Here's a gist with an example of how I am using the extendRoutes hook of the vite-plugin-pages plugin to generate a search index. In the example I've created a Vite plugin that wraps the vite-plugin-pages plugin to parse Vue SFC route files and pull searchable text out of it.

https://gist.github.com/andrewcourtice/748a6cb52e75b58a63727d712bb3b817

One of the pain points of the pages plugin extendRoute hook is that the only parameter it provides is the Vue route object. It would be useful (for this particular case at least) to also have access to the full file path of the file this route was generated for in the extendRoute hook. I've found a way around it by joining the relative route path to the current working directory but that may not work in all cases so it would be useful to just have the absolute file path available in the hook.

Thanks again 😄

Copy link
Owner

posva commented Feb 16, 2023

I see. The vite-plugin-pages-sitemap will require a different plugin as that one relies on the array of routes in the format of vite-plugin-pages and this format is different but everything should be available.

In the current version you already have access to the absolute paths with route.components.get('default') (or any other view name). If you give it a try and find missing things or unclear parts, let me know.

@jods4
Copy link

jods4 commented Feb 16, 2023

@posva I don't have time right now to look at the new functions, but it's great news!

To answer your request, here are functions I use in a real project with vite-plugin-pages:

pages({
      // ...most config removed for brievety...
      routeBlockLang: "yaml",
      
      extendRoute(route) {
        // nav is written as a path in <route>, but it should be an array of segments:
        if (route.nav) route.nav = route.nav.split("/");
        
        // For convenience, we set meta-properties directly in <route> but they should be under `meta`
        if (!route.meta) route.meta = {};        
        moveToMeta(route, "nav");
        moveToMeta(route, "auth");
        moveToMeta(route, "order");
      },
      
      onRoutesGenerated(routes) {
        // We filter routes included in app at build-time, based on their `env` and current build "mode" (basically target app).
        routes = routes.filter(r => !r.env || r.env === mode);

        // Hack to read an automatically generated list of roles (auth/security), used in loop below.
        const rolesTs = readFileSync("./Services/roles.ts", "utf8");
        const roles = new Map(
          Array.from(rolesTs.matchAll(/^\s*(\w+)\s*=\s*"([^"]+)"/gm), r => [
            r[1],
            r[2],
          ]),
        );

        for (const r of routes) {
          // Remove from runtime properties that were only required for build-time filtering above
          delete r.env;  

          if (r.meta?.auth) {
            const roleValue = roles.get(r.meta.auth);
            // Validate role exists (no typo, <route> is not type checked)
            if (!roleValue)
              logger.warn(`Unknown auth ${r.meta.auth} in route ${r.meta.nav}`);
            // Translate string into role id
            r.meta.auth = roleValue;
          }
        }

        return routes;
      },
    }),

Copy link
Owner

posva commented Feb 16, 2023

Thanks for the feedback. I think this one should be easy to migrate:

    VueRouter({
      // ...most config removed for brievety...
      routeBlockLang: 'yaml',

      extendRoute(route) {
        // TODO: probably let user to access the overrides directly with a type override? or host it somewhere
        const overrides: Record<string, unknown> = route.node.value
          .overrides as any

        // We filter routes included in app at build-time, based on their `env` and current build "mode" (basically target app).
        if (overrides.env !== mode) {
          // also removes the children
          route.delete()
          return
          // Another possibility to implement this would be to change the `exclude` option depending on the mode but this only work if pages are organized in folders.
        }

        if (overrides.nav) {
          // nav is written as a path in <route>, but it should be an array of segments:
          route.addToMeta({ nav: overrides.nav.split('/') })
        }

        // For convenience, we set meta-properties directly in <route> but they should be under `meta`
        route.addToMeta({
          auth: overrides.auth,
          order: overrides.order,
        })
      },

      beforeWriteFiles(routes) {
        // Hack to read an automatically generated list of roles (auth/security), used in loop below.
        const rolesTs = readFileSync('./Services/roles.ts', 'utf8')
        const roles = new Map(
          Array.from(rolesTs.matchAll(/^\s*(\w+)\s*=\s*"([^"]+)"/gm), (r) => [
            r[1],
            r[2],
          ])
        )

        for (const r of routes) {
          if (r.meta?.auth) {
            const roleValue = roles.get(r.meta.auth)
            // Validate role exists (no typo, <route> is not type checked)
            if (!roleValue)
              logger.warn(`Unknown auth ${r.meta.auth} in route ${r.meta.nav}`)
            // Translate string into role id
            r.addToMeta({ auth: roleValue })
          }
        }

      },
    }),

There are some improvements to make like allow access to the overrides without using the private node property but this helps to move the API forward.

@settings settings bot removed the enhancement label Feb 21, 2024
@posva posva added the ⚡️ enhancement improvement over an existing feature label Feb 21, 2024
@gduliscouet-ubitransport
Copy link

gduliscouet-ubitransport commented Mar 6, 2024

I'm adding here our use case:

Context

We are using unplugin-vue-router for having a typed router only, not for generating paths and route names from our file structure. This is mostly because we don't control the route paths, they must match the legacy paths we are progressively migrating from: they are long, boring and complex.

Our issue

We have pages that are available on multiple "product"s, but that use the same component. For example a list of transport lines is available on:

  • the main product (oriented for admin): /network/network_lines
  • the p_urban product (oriented for urban transport): /p_urban/p_urban_lines/index/:networkId (you access the lines by network)
  • the p_school product (oriented for school transport): /p_shool/p_school_lines/index/:networkId

Behind this:

  • we have a mechanisim in our links to keep the active product: for example, on the p_urban list of lines the link to the view of a line is to the p_urban view of a line url: /p_urban/p_urban_lines/:lineId
  • we define a different acl key on each page for access control: meta: { acl: 'p_urban_list_lines' }

What we tried, to define a single page for those 3 urls:

  • adding some parameter in the path: it would have work with better formed path, not in our case
  • using aliases: it didn't work well with having different acl meta and navigating using the route names

Our need

Being able to define multiple routes for the same page component, at build time.
For now we have a LineList.vue to define the main product route (/network/network_lines), and PUrbanLineList.vue imports LineList.vue

PUrbanLinesList.vue:

<script lang="ts" setup>
definePage({
    name: 'PUrbanLineList',
    path: '/p_urban/p_urban_lines/index/:networkId',
    meta: {
        acl: 'p_urban_list_lines',
        icon: 'alt_route',
    },
})
</script>

<template>
    <LinesList />
</template>

Possible solutions

  1. Being able to define a list of pages:
definePage([
  {
      name: 'MainLineList',
      path: '/network/network_lines',
      meta: {
          acl: 'main_list_lines',
          icon: 'alt_route',
  },
  {
      name: 'PUrbanLineList',
      path: '/p_urban/p_urban_lines/index/:networkId',
      meta: {
          acl: 'p_urban_list_lines',
          icon: 'alt_route',
  },
])
  1. Being able to use beforeWriteFiles for that (I'm not sure if it is alread possible ?)

@posva
Copy link
Owner

posva commented Mar 7, 2024

@gduliscouet-ubitransport Regarding the possible solutions, you can already use beforeWriteFiles to copy a route that exists.
Note that in the example you showed, one of your paths has a param while the other doesn't.

A simpler alternative is to create two different page components that render the same component

@posva posva added 💬 discussion topic that requires further discussion and removed ⚡️ enhancement improvement over an existing feature labels Mar 7, 2024
@gduliscouet-ubitransport

Thank you for the response @posva !

A simpler alternative is to create two different page components that render the same component

So this is what we do today, it is just a bit messy because in our case we have to create a lot of .vue files. That is why an approach where we could list them in an array would be simpler for us.

With beforeWriteFiles I didn't understand from the doc how I could add the PUrbanLineList route, pointing to LineList.vue, but with a different route name and meta ?

@posva
Copy link
Owner

posva commented May 27, 2024

Tracking the docs updates in https://github.com/users/posva/projects/2/views/1?query=is%3Aopen+sort%3Aupdated-desc&pane=issue&itemId=64673200

@gduliscouet-ubitransport tree.insert() returns an editable tree node that you can update, similar to a route record. I think that your current solution of importing LineList.vue is the best. If you want to add multiple routes pointing to the same component, just call tree.insert() multiple times

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💬 discussion topic that requires further discussion
Projects
Archived in project
Development

No branches or pull requests

6 participants