posva / unplugin-vue-router

Next Generation file based typed routing for Vue Router
https://uvr.esm.is
MIT License
1.68k stars 83 forks source link

Vite hmr doesn't work with error: ReferenceError: Cannot access '_page_0' before initialization with unplugin-auto-import #132

Closed xiangnanscu closed 5 months ago

xiangnanscu commented 1 year ago

When you modify template, the hmr doesn't work with error: ReferenceError: Cannot access '_page_0' before initialization. you must press F5 to refresh the page.

<script setup>
const route = useRoute();
</script>
<template>
  <div>foo</div>
</template>

But if you manually import useRoute, this problem doesn't exist

<script setup>
import { useRoute } from "vue-router";
const route = useRoute();
</script>
<template>
  <div>foo</div>
</template>

router.ts

import { createRouter, createWebHashHistory } from "vue-router";
import { routes } from "vue-router/auto/routes";

const router = createRouter({
  history: createWebHashHistory(import.meta.env.BASE_URL),
  routes: [...routes],
});
export default router;

vite.config.ts

import { readFileSync } from "fs";
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import VueRouter from "unplugin-vue-router/vite";
import {
  VueRouterAutoImports,
  getPascalCaseRouteName,
} from "unplugin-vue-router";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
import VitePluginOss from "vite-plugin-oss";
import * as dotenv from "dotenv";
const dotenvExpand = require("dotenv-expand");

const { parsed: exposedEnvs } = dotenvExpand.expand({
  ...dotenv.config({
    override: false,
    path: ".env",
  }),
  ignoreProcessEnv: true,
});

const env = process.env;
for (const key of Object.keys(env).sort()) {
  // console.log(key, env[key]);
}
const VITE_PROXY_PREFIX = process.env.VITE_PROXY_PREFIX || "/toXodel";
const VITE_PROXY_PREFIX_REGEX = new RegExp("^" + VITE_PROXY_PREFIX);
const VITE_APP_NAME = process.env.VITE_APP_NAME;
const PROD_URL = `https:${env.ALIOSS_URL}${VITE_APP_NAME}/`;

const baseUrl = env.NODE_ENV == "production" ? PROD_URL : "/";

const plugins = [
  Components({
    dirs: ["./components"],
    extensions: ["vue"],
    dts: "./unplugin/components.d.ts",
    resolvers: [AntDesignVueResolver()],
  }),
  VueRouter({
    // Folder(s) to scan for vue components and generate routes. Can be a string, or
    // an object, or an array of those.
    routesFolder: ["src/views", "views"],

    // allowed extensions to be considered as routes
    extensions: [".vue"],

    // list of glob files to exclude from the routes generation
    // e.g. ['**/__*'] will exclude all files starting with `__`
    // e.g. ['**/__*/**/*'] will exclude all files within folders starting with `__`
    exclude: [],

    // Path for the generated types. Defaults to `./typed-router.d.ts` if typescript
    // is installed. Can be disabled by passing `false`.
    dts: "./unplugin/typed-router.d.ts",

    // Override the name generation of routes. unplugin-vue-router exports two versions:
    // `getFileBasedRouteName()` (the default) and `getPascalCaseRouteName()`. Import any
    // of them within your `vite.config.ts` file.
    getRouteName: getPascalCaseRouteName,

    // Customizes the default langage for `<route>` blocks
    // json5 is just a more permissive version of json
    routeBlockLang: "json5",

    // Change the import mode of page components. Can be 'async', 'sync', or a function with the following signature:
    // (filepath: string) => 'async' | 'sync'
    importMode: "sync",
  }),
  vue(),
  vueJsx({
    // https://github.com/vuejs/babel-plugin-jsx
    // https://github.com/vitejs/vite/tree/main/packages/plugin-vue-jsx
  }),
  AutoImport({
    //https://github.com/antfu/unplugin-auto-import#configuration
    eslintrc: {
      enabled: true, // Default `false`
      filepath: "./unplugin/.eslintrc-auto-import.json", // Default `./.eslintrc-auto-import.json`
      globalsPropValue: true, // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
    },
    imports: [
      "vue",
      "pinia",
      // "vue-router", // comment this if using VueRouterAutoImports
      VueRouterAutoImports,
      { "vue-router/auto": ["useLink"] },
      "@vueuse/core",
      {
        // "@vueuse/core": [
        //   // named imports
        //   "useMouse", // import { useMouse } from '@vueuse/core',
        //   // "useFetch",
        //   // ["useFetch", "useMyFetch"], // import { useFetch as useMyFetch } from '@vueuse/core',
        // ],
        axios: [
          // default imports
          ["default", "axios"], // import { default as axios } from 'axios',
        ],
      },
    ],
    dts: "./unplugin/auto-imports.d.ts",
    vueTemplate: true,
    include: [
      /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
      /\.vue$/,
      /\.vue\?vue/, // .vue
    ],
    dirs: [
      "./components", // only root modules
      "./composables", // only root modules
      "./stores/**", // all nested modules
    ],
  }),
  {
    // this plugin handles ?b64 tags
    name: "vite-b64-plugin",
    transform(code, id) {
      if (!id.match(/\?b64$/)) return;
      // console.log(id, code);
      const path = id.replace(/\?b64/, "");
      const data = readFileSync(path, "base64");
      return `export default '${data}'`;
    },
  },
];
if (process.env.NODE_ENV == "production") {
  plugins.push(
    VitePluginOss({
      from: "./dist/**", // 上传那个文件或文件夹
      dist: `/${VITE_APP_NAME}`, // 需要上传到oss上的给定文件目录
      region: env.ALIOSS_REGION,
      accessKeyId: env.ALIOSS_KEY,
      accessKeySecret: env.ALIOSS_SECRET,
      bucket: env.ALIOSS_BUCKET,
      // test: true, // 测试,可以在进行测试看上传路径是否正确, 打开后只会显示上传路径并不会真正上传。默认false
      // 因为文件标识符 "\"  和 "/" 的区别 不进行 setOssPath配置,上传的文件夹就会拼到文件名上, 丢失了文件目录,所以需要对setOssPath 配置。
      setOssPath: (filePath) => {
        const index = filePath.lastIndexOf("dist");
        const Path = filePath.substring(index + 4, filePath.length);
        return Path.replace(/\\/g, "/");
      },
    })
  );
}
const proxyServer = `http://${env.NGINX_server_name}:${env.NGINX_listen}`;
export default defineConfig({
  base: baseUrl,
  define: {
    "process.env": exposedEnvs,
  },
  plugins,
  optimizeDeps: {
    include: ["vue"],
  },
  resolve: {
    alias: {
      "~/": fileURLToPath(new URL("./src", import.meta.url)) + "/",
    },
  },
  css: {
    preprocessorOptions: {
      less: {
        javascriptEnabled: true,
      },
    },
  },
  server: {
    // https://vitejs.dev/config/server-options.html#server-proxy
    // https://github.com/http-party/node-http-proxy#options
    port: Number(env.VITE_APP_PORT) || 5173,
    strictPort: true,
    proxy: {
      [VITE_PROXY_PREFIX]: {
        target: proxyServer,
        changeOrigin: true,
        rewrite: (path) => path.replace(VITE_PROXY_PREFIX_REGEX, ""),
      },
    },
  },
});
posva commented 1 year ago

Do you have a reproduction without the vite-plugin-oss and ant?

xiangnanscu commented 1 year ago

@posva click here, click Test and modify src/views/Test.vue's template.

posva commented 1 year ago

The link seems private

xiangnanscu commented 1 year ago

@posva sorry, plz refer this https://github.com/xiangnanscu/v

posva commented 1 year ago

When using sync mode there seems to be (I think) a circular dependency somewhere creating the error you see when also using useRoute() with auto import (from vue-router/auto). Similar to issues reported at https://github.com/vitejs/vite/issues/3033

Using async (the default) doesn't show this problem. Note that in your repro you are also importing the routes and calling vue-router createRouter() but you should do this instead (as shown in docs):

import { createRouter, createWebHashHistory } from 'vue-router/auto'

const router = createRouter({
  history: createWebHashHistory(import.meta.env.BASE_URL),
  extendRoutes(routes) {
    return routes
  }
})
export default router

Unfortunately, this doesn't fix the problem. I recommend you to use async mode in the meantime or to toggle it for dev builds with importMode: process.env.NODE_ENV === 'production' ? 'sync' : 'async' or similar.

Contribution welcome!

xenolithviktor commented 8 months ago

@posva Any possibility we could fix this soon? Since 0.8 this has become a bigger problem for us since we are now forced to import from 'vue-router/auto' to get typing on useRoute. This forces us to use 'async' in development, leading to inconsistent application behavior between dev and production.

I'd be happy to make another PR, but I need to know which path to take.

Here are some different ways to solve it:

  1. Use Promise.resolve like in my previous PR. A bit hacky, but it only runs in dev and keeps HMR happy. Not a breaking change.
  2. Turn routes in 'vue-router/auto/routes' into a function instead of a static array. Maybe also a bit hacky? This solves the HMR problem, but the circular import is still there I guess. Not a breaking change.
  3. Moving createRouter into its own sub-module, like 'vue-router/auto/config' or similar. AFAIK the only reason we import routes into 'vue-router/auto' is for createRouter. If we move this to its own sub-module that is not imported in pages we would get rid of the circular dependency entirely. This is probably the cleanest approach, but would be a breaking change as everyone would have to update their import where they set up the router.
posva commented 8 months ago

@xenolithviktor I need to investigate more. For the moment, I recommend you to use any of the versions with pnpm patch or patch-package.

I will focus on fixing this and #5 at some point, but I need to focus on other features and bugs now.

xenolithviktor commented 8 months ago

@posva I understand. When you do get the time, I'll be happy to assist in any way I can.

Previously, the typed-routes.d.ts would declare typings for the 'vue-router' module which made it possible to import from there as a workaround. Would it be possible to get that back somehow with an extra d.ts file? If so, what would we put in it? I tried copying the declaration from a previous version of unplugin-vue-router but it does not seem to work..

Patching should be a last resort 😅

roos-robert commented 8 months ago

@posva Any updates regarding this? @xenolithviktor has put forward he is happy to help with a solution and just need your blessing on which way to move forward with a PR.

posva commented 8 months ago

You don't need my blessing in any way 😅 This is open source, feel free to work on it and submit a PR. I'm busy with other matters that affect more users, so using async mode or a local patch is the preferred way until a proper fix is found

xenolithviktor commented 8 months ago

Here is a little workaround that patches the routes array to be a function using a Vite-plugin. (Add to vite config plugins)

{
  // Hacky workaround until HMR in sync mode is fixed, see: https://github.com/posva/unplugin-vue-router/issues/132
  name: 'unplugin-vue-router-hmr-workaround',
  apply: 'serve',
  transform(code, id) {
    if (id === 'virtual:vue-router/auto') {
      return {
        code: code.replace('extendRoutes(routes) : routes', 'extendRoutes(routes()) : routes()')
      }
    }
    if (id === 'virtual:vue-router/auto-routes') {
      return {
        code: code.replace('export const routes = [', 'export const routes = () => [')
      }
    }
  }
}

This only runs on serve and makes HMR happy by using a function instead of defining the routes array directly in the module, which removes the cyclic dependency error in Vite.

Note that this will only work when using createRouter from vue-router/auto, and not if importing the routes directly from vue-router/auto-routes.

In the future, in my opinion, it would probably be best to remove the custom createRouter (and the import of vue-router/auto-routes) from vue-router/auto completely, and instead have the users import the routes manually when they set up the router. We don't want vue-router/auto-routes to be imported on every page that uses things like useRouter() or useRoute() as this is what introduces the cyclic problem.

ferferga commented 7 months ago

@xenolithviktor Rollup's still report CIRCULAR_DEPENDENCY warnings, both with your PR and with your plugin workaround (HMR is working fine for me, it's just production where I'm having issues), so none of them are the real solution to this issue.

I've been investigating further and I'm also onboard that the best solution (albeit it's a minor inconvinient, but just for the first-time setup) is to force users to import the routes manually, like in the layout example in README.

posva commented 5 months ago

The more I think about this, the more I think routes shouldn't be automatically added. Making the import of routes explicit would remove the circular dependency issue

ferferga commented 5 months ago

Not sure about others, but on 0.9.1 nothing changed about circular imports. @posva Let me know if you want another issue opened for this or "recycling" this one is good

In a quick glance, I think the issue is when a page/component inside a page imports a JS/TS module that imports the global instance of Vue Router:

// router.ts
export const router = createRouter(...)
// external.ts
import { router } from '@/router.ts'

...
// module contents
...
// This module is imported by a Vue Component/Page
posva commented 5 months ago

You’re creating a cyclic import. Use useRouter

ferferga commented 5 months ago

@posva But then how I can use the router outside the Vue application? I understand that, if external.ts were consumed just by Vue components, it would work right, but that's not the case (it's also imported by other external modules). How we should do it in those situations?

If I do the following:

// @/plugins/router/index.ts
import {
  createRouter,
  createWebHashHistory,
  createWebHistory
} from 'vue-router';
import type { RouteNamedMap, _RouterTyped } from 'unplugin-vue-router/types';

export const router = createRouter({
  history: createWebHashHistory(),
  routes: [],
}) as _RouterTyped<RouteNamedMap>;

And somewhere else:

// main.ts
import { routes } from 'vue-router/auto-routes';

for (const route of routes) {
  router.addRoute(route);
}

There are no issues, but of course this is not ideal given the need for the casting, it's just a workaround.

vite-plugin-pages does not have the issue either. I believe the only real solution to these kind of circular imports in all use cases is to get rid of the main virtual module (vue-router/auto) and use vue-router itself.