github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/ui/plugins/vite-plugin-next-react-router/index.js (about)

     1  // src/context.ts
     2  import path4 from 'path'
     3  import fs2 from 'fs'
     4  
     5  // src/codegen.ts
     6  import fs from 'fs'
     7  import path3 from 'path'
     8  
     9  // src/resolver.ts
    10  import path2 from 'path'
    11  import { normalizePath } from 'vite'
    12  import fg from 'fast-glob'
    13  
    14  // src/utils.ts
    15  import path from 'path'
    16  import consola from 'consola'
    17  import { upperFirst, camelCase } from 'lodash'
    18  
    19  // package.json
    20  var name = 'vite-plugin-next-react-router'
    21  
    22  // src/const.ts
    23  var MATCH_ALL_ROUTE = '*'
    24  var DEFAULT_EXT = ['tsx', 'ts', 'jsx', 'js']
    25  var DEFAULT_PAGE_DIR = 'src/pages'
    26  
    27  // src/utils.ts
    28  var debug = (message, ...args) => {
    29    if (process.env.DEBUG === name) {
    30      consola.info(message, ...args)
    31    }
    32  }
    33  function isCatchAll(filename) {
    34    return /^\[\.{3}/.test(filename)
    35  }
    36  function isDynamic(filename) {
    37    return /^\[(.+)\]$/.test(filename)
    38  }
    39  function normalizeFilenameToRoute(filename) {
    40    if (isCatchAll(filename)) {
    41      return MATCH_ALL_ROUTE
    42    }
    43    if (filename === 'index') {
    44      return '/'
    45    }
    46    return isDynamic(filename) ? parameterizeDynamicRoute(filename) : filename
    47  }
    48  function parameterizeDynamicRoute(s) {
    49    return s.replace(/^\[(.+)\]$/, (_, p) => `:${p}`)
    50  }
    51  function normalizeDirPathToRoute(dirPath) {
    52    return dirPath
    53      .split('/')
    54      .map(s => (isDynamic(s) ? parameterizeDynamicRoute(s) : s))
    55      .join('/')
    56  }
    57  function normalizePathToRoute(p) {
    58    const { dir, name: name2 } = path.parse(p)
    59    const route = normalizeFilenameToRoute(name2)
    60    if (route === MATCH_ALL_ROUTE) {
    61      return route
    62    }
    63    return path.resolve(path.join('/', normalizeDirPathToRoute(dir), route))
    64  }
    65  function countLength(p) {
    66    return path.resolve(p).split('/').filter(Boolean).length
    67  }
    68  function sorter(a, b) {
    69    const len = countLength(a) - countLength(b)
    70    if (len !== 0) {
    71      return len
    72    }
    73    return a.localeCompare(b)
    74  }
    75  function sortRoutes(routes) {
    76    return [...routes].sort(sorter)
    77  }
    78  function getComponentName(filePath) {
    79    const segments = filePath.split(path.sep)
    80    const extname = path.extname(filePath)
    81    const fileName = path.basename(filePath, extname)
    82    segments[segments.length - 1] = fileName
    83    const name2 = segments.reduce((acc, segment) => {
    84      if (isDynamic(segment)) {
    85        return acc + upperFirst(camelCase(segment.replace(/^\[(.+)\]$/, '$1')))
    86      }
    87      return acc + upperFirst(camelCase(segment))
    88    }, '')
    89    return name2
    90  }
    91  
    92  // src/resolver.ts
    93  function resolvePages(options) {
    94    const { root, pageDir, extensions } = options
    95    const files = scan(pageDir, extensions, root)
    96    return fileToRouteMap(files)
    97  }
    98  function resolveOptions(userOptions, viteRoot) {
    99    var _a, _b, _c, _d
   100    const root = viteRoot != null ? viteRoot : normalizePath(process.cwd())
   101    return {
   102      root,
   103      async:
   104        (_a = userOptions == null ? void 0 : userOptions.async) != null
   105          ? _a
   106          : true,
   107      pageDir:
   108        (_b = userOptions == null ? void 0 : userOptions.pageDir) != null
   109          ? _b
   110          : DEFAULT_PAGE_DIR,
   111      extensions:
   112        (_c = userOptions == null ? void 0 : userOptions.extensions) != null
   113          ? _c
   114          : DEFAULT_EXT,
   115      output:
   116        (_d = userOptions == null ? void 0 : userOptions.output) != null
   117          ? _d
   118          : path2.join(root, 'src', 'routes.tsx'),
   119    }
   120  }
   121  function scan(targetDir, extensions, root) {
   122    const fullPathOfTargetDir = path2.resolve(root, targetDir)
   123    return fg.sync([`**/*.{${extensions.join()}}`, `!_layout.*`], {
   124      onlyFiles: true,
   125      cwd: fullPathOfTargetDir,
   126    })
   127  }
   128  function fileToRouteMap(files) {
   129    const map = new Map()
   130    files.forEach(file => {
   131      const route = normalizePathToRoute(file)
   132      map.set(route, file)
   133    })
   134    return map
   135  }
   136  function resolveRoutes(map, options) {
   137    const sortedRoutes = sortRoutes([...map.keys()])
   138    return sortedRoutes.map(route => {
   139      const filePath = map.get(route)
   140      const absolutePath = path2.resolve(options.pageDir, filePath)
   141      const routeObj = {
   142        path: route,
   143        componentPath: absolutePath,
   144        componentName: getComponentName(filePath),
   145      }
   146      if (path2.basename(filePath, path2.extname(filePath)) === 'index') {
   147        routeObj.index = true
   148      }
   149      return routeObj
   150    })
   151  }
   152  function resolveGlobalLayout(options) {
   153    const globalLayoutFiles = fg.sync(
   154      [`_layout.{${options.extensions.join()}}`],
   155      {
   156        onlyFiles: true,
   157        cwd: options.pageDir,
   158      }
   159    )
   160    if (globalLayoutFiles.length === 1) {
   161      const filePath = globalLayoutFiles[0]
   162      return {
   163        componentPath: path2.resolve(options.pageDir, globalLayoutFiles[0]),
   164        componentName: 'GlobalLayout',
   165        path: filePath,
   166      }
   167    } else if (globalLayoutFiles.length > 1) {
   168      throw new Error('Multiple _layout files found')
   169    }
   170    return null
   171  }
   172  
   173  // src/template.ts
   174  var generateRoutesCode = ({
   175    layoutImport: layoutImport2,
   176    pageImports,
   177    layoutElement,
   178    routes,
   179    staticPageMetaImports,
   180    pages,
   181  }) => `
   182  import React from 'react';
   183  ${layoutImport2}
   184  ${staticPageMetaImports}
   185  ${pageImports}
   186  
   187  export const routes = [
   188    {
   189      path: '/',
   190      element: ${layoutElement},
   191      children: [
   192        ${routes}
   193      ]
   194    }
   195  ]
   196  
   197  export const pages = [
   198    ${pages}
   199  ]
   200  `
   201  
   202  // src/codegen.ts
   203  function generateComponentCall(route, options) {
   204    return `<${
   205      options.async
   206        ? `Dynamic${route.componentName}`
   207        : `Static${route.componentName}`
   208    } />`
   209  }
   210  function stripExt(p) {
   211    return p.replace(path3.extname(p), '')
   212  }
   213  function transformToRelativePath(to, from) {
   214    return './' + stripExt(path3.relative(path3.dirname(path3.resolve(from)), to))
   215  }
   216  var dynamicPageImports = (routes, options) =>
   217    routes.reduce((acc, route) => {
   218      return (
   219        acc +
   220        `const Dynamic${
   221          route.componentName
   222        } = React.lazy(() => import('${transformToRelativePath(
   223          route.componentPath,
   224          options.output
   225        )}'));
   226  `
   227      )
   228    }, '')
   229  var staticPageImports = (routes, options) =>
   230    routes.reduce((acc, route) => {
   231      return (
   232        acc +
   233        `import Static${route.componentName} from '${transformToRelativePath(
   234          route.componentPath,
   235          options.output
   236        )}'
   237  `
   238      )
   239    }, '')
   240  var staticMetaImports = (routes, options) => {
   241    return routes.reduce((acc, route) => {
   242      var _a
   243      const routeComponentCode = fs.readFileSync(route.componentPath, 'utf8')
   244      if (routeComponentCode.includes('export const meta')) {
   245        return (
   246          acc +
   247          `import { meta as Static${
   248            route.componentName
   249          }Meta } from '${transformToRelativePath(
   250            route.componentPath,
   251            (_a = options.output) != null
   252              ? _a
   253              : path3.join(options.root, 'src', 'pages.ts')
   254          )}'
   255  `
   256        )
   257      }
   258      return acc
   259    }, '')
   260  }
   261  var layoutImport = options => {
   262    const layout = resolveGlobalLayout(options)
   263    let imports = ''
   264    if (!layout) {
   265      imports += `import { Outlet } from 'react-router-dom';`
   266    }
   267    imports = layout
   268      ? `import ${layout.componentName} from '${transformToRelativePath(
   269          layout.componentPath,
   270          options.output
   271        )}'`
   272      : ''
   273    const element = `<${layout ? layout.componentName : 'Outlet'} />`
   274    return { imports, element }
   275  }
   276  var routeObjects = (routes, options) =>
   277    routes
   278      .map(route => {
   279        return `{ path: '${route.path}', element: ${generateComponentCall(
   280          route,
   281          options
   282        )}, ${route.index ? 'index: true' : ''}},
   283  `
   284      })
   285      .join(' '.repeat(6))
   286      .trim()
   287  var pageObjects = routes =>
   288    routes
   289      .filter(r => r.path !== '*')
   290      .map(route => {
   291        const routeComponentCode = fs.readFileSync(route.componentPath, 'utf8')
   292        if (routeComponentCode.includes('export const meta')) {
   293          return `{ route: '${route.path}', meta: Static${route.componentName}Meta },
   294  `
   295        }
   296        return `{ route: '${route.path}' },
   297  `
   298      })
   299      .join(' '.repeat(2))
   300      .trim()
   301  function generateRoutesModuleCode(routes, options) {
   302    const { imports, element } = layoutImport(options)
   303    const staticPageMetaImports = staticMetaImports(routes, options)
   304    const pages = pageObjects(routes)
   305    return generateRoutesCode({
   306      layoutImport: imports,
   307      pageImports: options.async
   308        ? dynamicPageImports(routes, options)
   309        : staticPageImports(routes, options),
   310      layoutElement: element,
   311      routes: routeObjects(routes, options),
   312      pages,
   313      staticPageMetaImports,
   314    })
   315  }
   316  
   317  // src/context.ts
   318  function isTarget(p, options) {
   319    return (
   320      p.startsWith(path4.resolve(options.pageDir)) &&
   321      options.extensions.some(ext => p.endsWith(ext))
   322    )
   323  }
   324  var Context = class {
   325    constructor(userOptions) {
   326      this._pages = new Map()
   327      this._server = null
   328      this.root = '.'
   329      this._userOptions = userOptions
   330    }
   331    resolveOptions() {
   332      this._resolvedOptions = resolveOptions(this._userOptions, this.root)
   333    }
   334    search() {
   335      if (!this._resolvedOptions) {
   336        this.resolveOptions()
   337      }
   338      this._pages = resolvePages(this._resolvedOptions)
   339      debug('pages: ', this._pages)
   340    }
   341    configureServer(server) {
   342      this._server = server
   343      this._server.watcher.on('unlink', filaPath =>
   344        this.invalidateAndRegenerateRoutes(filaPath)
   345      )
   346      this._server.watcher.on('add', filaPath =>
   347        this.invalidateAndRegenerateRoutes(filaPath)
   348      )
   349    }
   350    invalidateAndRegenerateRoutes(filaPath) {
   351      if (!isTarget(filaPath, this._resolvedOptions)) {
   352        return
   353      }
   354      this._pages.clear()
   355      this.generateRoutesModuleCode()
   356    }
   357    generateRoutesModuleCode() {
   358      if (this._pages.size === 0) {
   359        this.search()
   360      }
   361      const routes = resolveRoutes(this._pages, this._resolvedOptions)
   362      const code = generateRoutesModuleCode(routes, this._resolvedOptions)
   363      debug('generateVirtualRoutesCode: \n', code)
   364      fs2.writeFileSync(this._resolvedOptions.output, code)
   365    }
   366  }
   367  
   368  // src/index.ts
   369  function reactRouterPlugin(userOptions) {
   370    const ctx = new Context(userOptions)
   371    return {
   372      name: 'vite-plugin-next-router',
   373      enforce: 'pre',
   374      async configResolved({ root }) {
   375        ctx.root = root
   376        ctx.resolveOptions()
   377        ctx.search()
   378        ctx.generateRoutesModuleCode()
   379      },
   380      configureServer(server) {
   381        ctx.configureServer(server)
   382      },
   383    }
   384  }
   385  export { reactRouterPlugin }