Closed1

next.jsのファイル一覧からUrlObject形式の型生成するやつ作る

terrierscriptterrierscript
  • next.jsでファイル一覧から型つくりたかった
  • パスを生成するライブラリに頼らずなるべくnext.js wayを補助する方向にしたかった
  • template literal typeでやるのはちょっと難しくて挫折した
    • /を含まないstring型みたいなのを作れずパズルが終了した
  • UrlObject形式なら割と簡易に作れそうな感じだったのでそっちにした

installation

$ yarn add recursive-readdir

生成スクリプト

// bin/typegen.js
const recursiveReaddir = require("recursive-readdir")
const {getRouteRegex}= require("next/dist/next-server/lib/router/utils/route-regex")
const path = require("path")

const convertToUrlObject = (str) => {
  const r = getRouteRegex(str)
  const queries = Object.entries(r.groups).map(([name, opts]) => {
    const queryKey = opts.optional ? `${name}?` : name
    const valueKey = opts.repeat ? `ReadonlyArray<string> | ReadonlyArray<number>` :`string | number` 
    return `${queryKey} : ${valueKey}`
  })
  const q = queries.length > 0 ? `query: { ${queries.join(",")} }` : null
  const typeValue = [ `pathname: "/${str}"`, q ].filter(x => !!x).join(" , ")
  return `UrlObject & { ${typeValue} }`
}

const execute = async (sourceDir) => {
  const files = await recursiveReaddir(path.join(sourceDir,"pages"))
  const baseDir = path.normalize(sourceDir)
  const regexp = new RegExp(`^${baseDir}/pages/`)
  const pathnames = files
    .map(file => {
      const pathname = file
        .replace(regexp, "") // remove basedir
        .replace(/.[jt]sx?$/, "") // remove exteension
      return pathname
    })
    .filter(file => !/^_.+/.test(file)) // filter _app / _document
  
  const paths = pathnames.map(pathname => {
    const paths = pathname.split("/")
    if (paths[paths.length - 1] === "index") { // /index -> /
      const omitIndex = paths.slice(0, paths.length - 1).join("/")
      return omitIndex
    }
    return pathname
  })
  const q = paths.map(str => convertToUrlObject(str))
    .join("\n | ")
  const types = [
    `// auto-generated`,
    `type AppUrlObject = ${q}`,
  ].join("\n")
  console.log(types)
}

execute("./")

// srcディレクトリに配置していたらこう
// execute("./src")

実行

これを実行すると、こういうのが出てくる

% node bin/typegen.js                                                     
// auto-generated
type AppUrlObject = UrlObject & { pathname: "/" }
 | UrlObject & { pathname: "/users" }
 | UrlObject & { pathname: "/api/hello" }
 | UrlObject & { pathname: "/api/rest/[...slug]" , query: { slug : ReadonlyArray<string> | ReadonlyArray<number> } }
 | UrlObject & { pathname: "/api/rest2/[[...slug]]" , query: { slug? : ReadonlyArray<string> | ReadonlyArray<number> } }
 | UrlObject & { pathname: "/users/[user_id]" , query: { user_id : string | number } }
 | UrlObject & { pathname: "/users/[user_id]/posts" , query: { user_id : string | number } }
 | UrlObject & { pathname: "/users/[user_id]/posts/[post_id]/comments" , query: { user_id : string | number,post_id : string | number } }

使い方

あとこんな感じで使える(はず)


// auto-generated (コピペしてきたもの)
type AppUrlObject = UrlObject & { pathname: "/" }
  | UrlObject & { pathname: "/users" }
  | UrlObject & { pathname: "/api/hello" }
  | UrlObject & { pathname: "/api/rest/[...slug]", query: { slug: ReadonlyArray<string> | ReadonlyArray<number> } }
  | UrlObject & { pathname: "/api/rest2/[[...slug]]", query: { slug?: ReadonlyArray<string> | ReadonlyArray<number> } }
  | UrlObject & { pathname: "/users/[user_id]", query: { user_id: string | number } }
  | UrlObject & { pathname: "/users/[user_id]/posts", query: { user_id: string | number } }
  | UrlObject & { pathname: "/users/[user_id]/posts/[post_id]/comments", query: { user_id: string | number, post_id: string | number } }

const AppLink: FC<{ href: AppUrlObject }> = ({ href }) => {
  return <div>
    <NextLink href={href}>{JSON.stringify(href)}</NextLink>
  </div>
}

export const SomeApp = () => {
  // const userId = 10
  // const url = `/users/${userId}/posts/` as const
  return <div>
    <AppLink href={{
      pathname: "/",
    }} />
    <AppLink href={{
      pathname: "/users/[user_id]",
      query: { user_id: 10 }
    }} />
    <AppLink href={{
      pathname: "/api/rest2/[[...slug]]",
      query: { slug: [10, 11] }
    }} />
  </div>
}

例えば下記のようなのは怒られる

    <AppLink href={{
      pathname: "/unknown/[user_id]", 
      query: { user_id: 10 }
    }} />
    <AppLink href={{
      pathname: "/users/[user_id]", 
    }} />
    <AppLink href={{
      pathname: "/api/rest2/[[...slug]]",
      query: { slug: 11 }
    }} />

もうちょっと緩めにしたければこんなふうにするのも可

type WeakAppUrl = AppUrlObject | string | UrlObject
このスクラップは2021/04/24にクローズされました