kahirokunn

Vue.jsとReactが大好きで毎日書いてます.

最近の日課はVue 3 エコシステムからStable releaseがされてないか確認すること

話1つ目

Vueのエコシステムの現時点の状況

[@vue/cli](https://www.npmjs.com/package/@vue/cli)
vue-cliでは既にvue2,vue3どちらでプロジェクトを作成するか選べる

[vue-loader@16.0.0-rc.1](https://www.npmjs.com/package/vue-loader)
[vuex@4.0.0-rc.1](https://www.npmjs.com/package/vuex)
[vue-router@4.0.0-rc.3](https://www.npmjs.com/package/vue-router)
[vee-validate@4.0.0-beta.19](https://www.npmjs.com/package/vee-validate)
[vue-apollo-composable@4.0.0-alpha.12](https://www.npmjs.com/package/@vue/apollo-composable)

[buefy](https://www.npmjs.com/package/buefy)
buefyは無償のメンテナで構成されており、その為Vue3対応に対して積極的に進められておらず、Vue3対応プロジェクト自体も開始できていない.
Issue: https://github.com/buefy/buefy/issues/2505

[vuetify](https://www.npmjs.com/package/vuetify)
VuetifyのロードマップではVuetify 3.0でVue3対応する予定です.
https://vuetifyjs.com/en/introduction/roadmap

[nuxt](https://github.com/nuxt/nuxt.js/issues/5708#issuecomment-668555312)
現在進行中

[storybook](https://www.npmjs.com/package/storybook)
StorybookのVue3対応は、Vueのエコシステムからrcが取れてから進みそうな気配があります.
rcが取れれば立候補者が恐らく登場し、v6.2.0から使えるようになりそうな気がしています.
※2020年11月15日時点
Vue3対応待ちライブラリと時点でリリースされているバージョン番号
バージョン番号がないのは、Vue3対応がいつされるかわからないものです.
[@vue/cli](https://www.npmjs.com/package/@vue/cli)
vue-cliでは既にvue2,vue3どちらでプロジェクトを作成するか選べる

[vue-loader@16.0.0-rc.1](https://www.npmjs.com/package/vue-loader)
[vuex@4.0.0-rc.1](https://www.npmjs.com/package/vuex)
[vue-router@4.0.0-rc.3](https://www.npmjs.com/package/vue-router)
※2020年11月15日時点
時点でリリースされている公式のエコシステムをいくつかピックアップ

ついに公式のエコシステムはついにrcまで来てる!

[vue-apollo-composable@4.0.0-alpha.12](https://www.npmjs.com/package/@vue/apollo-composable)

[buefy](https://www.npmjs.com/package/buefy)
buefyは無償のメンテナで構成されており、その為Vue3対応に対して積極的に進められておらず、Vue3対応プロジェクト自体も開始できていない.
Issue: https://github.com/buefy/buefy/issues/2505

[vuetify](https://www.npmjs.com/package/vuetify)
VuetifyのロードマップではVuetify 3.0でVue3対応する予定です.
https://vuetifyjs.com/en/introduction/roadmap

[nuxt](https://github.com/nuxt/nuxt.js/issues/5708#issuecomment-668555312)
現在進行中

[storybook](https://www.npmjs.com/package/storybook)
StorybookのVue3対応は、Vueのエコシステムからrcが取れてから進みそうな気配があります.
rcが取れれば立候補者が恐らく登場し、v6.2.0から使えるようになりそうな気がしています.
※2020年11月15日時点
時点でリリースされているサードパーティのエコシステムをいくつかピックアップ

サードパーティのVue3対応はこれからな感じ

StorybookはVue3対応してくれる人募集している

その他エコシステムもそういう方を募集している

つまり

コントリビュートチャンス!

皆でVue3を盛り上げよう!

話2つ目

react-router-route-generatorを作った話

経緯

諸事情により最近Reactを書いているのだが、auto routingか同等の物がReactで欲しかった.

でないと、創作意欲が凝らされた最高の戦闘力を持つpage達が誕生してしまう.
機械的に管理できるレールを引きたかった.

Next.jsとかにはこの手の仕組が搭載されているが、単体でそれだけをしてくれるライブラリが見当たらなかった.

なので、作成した.

仕様

仕様は、Next.jsのpagesと同じものを与えると、その通りにルーティングします.

もしサポートされてない機能で欲しいものがあったらIssue立ててくれたら素早く対応すると思います.

また、useParams hooks等で利用すると便利な型も吐いてくれます.

  • pages/post/[pid].tsx => /post/1, /post/abc
  • pages/post/[...slug].tsx => /post/a, /post/a/b, /post/a/b/c
import React from 'react';
import { Route } from 'react-router';
import loadable from '@loadable/component';

export type UseParamsType = {
  ['/users/[userId]']: { ['userId']: string };
};

export const RouteConfig = {
  ['/users/:userId']: loadable(() => import('@/pages/users/[userId]/index.tsx')),
  ['/users']: loadable(() => import('@/pages/users/index.tsx')),
  ['/']: loadable(() => import('@/pages/index.tsx')),
};

export default () => (
  <>
    <Route path="/users/:userId" component={RouteConfig['/users/:userId']} exact />

    <Route path="/users" component={RouteConfig['/users']} exact />

    <Route path="/" component={RouteConfig['/']} exact />
  </>
);

$ npm install -D react-router-route-generator
$ npx generate-routes

簡単に使える

実装は簡単なルールベースで最小限のコードで済む様にした

大体Next.jsのルールを満たすように正規表現で実装している.

詳しく聞きたい場合は、個別に聞いてくれると幸いです!

import glob from 'glob';
import compareFunc from 'compare-func';

function assertsHasValue<T>(
  value: T,
  errorMessage: string,
): asserts value is Exclude<T, null | undefined | void> {
  if (value === null || value === undefined) {
    throw new Error(errorMessage);
  }
}

function globSync(
  patterns: string | string[],
  ignorePatterns: string[],
): string[] {
  if (typeof patterns === 'string') {
    patterns = [patterns];
  }

  return patterns.reduce(
    (acc, pattern) =>
      acc.concat(glob.sync(pattern, { nodir: true, ignore: ignorePatterns })),
    [] as string[],
  );
}

type Slug = {
  name: string;
  isRest: boolean;
};
type Slugs = Slug[];

type MetaData = {
  component: string;
  urlPath: string;
  path: string;
  slugs?: Slugs;
  isLastOptional: boolean;
};

// 拡張子を取り除く
// .replace(/^(.*)\.(js|jsx|ts|tsx)$/, '$1')
function genRouteMetaData(componentPath: string, prefetch: boolean): MetaData {
  let urlPath = componentPath
    // 末尾の/index.{js,jsx,ts,tsx} を消す
    .replace(/^(.*)\/index\.(js|jsx|ts|tsx)$/, '$1')
    // 先頭の@/pagesを取り除く
    .replace(/^@\/pages(.*)$/, '$1');

  const isLastOptional = /^(.*)\.(js|jsx|ts|tsx)$/.test(urlPath);
  if (isLastOptional) {
    urlPath = urlPath.replace(/^(.*)\.(js|jsx|ts|tsx)$/, '$1');
  }
  return _calcRouteMetaData({
    path: urlPath,
    isLastOptional,
    component: `() => import(${
      prefetch ? '/* webpackPrefetch: true */' : ''
    } '${componentPath}')`
      // 拡張子を取り除く
      .replace(/^(.*)\.(js|jsx|ts|tsx)$/, '$1'),
    urlPath: urlPath === '' ? '/' : urlPath,
  });
}

// hoge/fuga/[piyo]/[piyopiyo] => hoge/fuga/:piyo/:piyopiyo
function _calcRouteMetaData(metaData: MetaData): MetaData {
  const result = /^(.*)\[(.*?)\](.*)$/.exec(metaData.urlPath);
  if (!result) {
    if (metaData.isLastOptional) {
      const items = metaData.urlPath.split('/');
      items[items.length - 1] = `${items[items.length - 1]}?`;
      metaData = {
        ...metaData,
        urlPath: items.join('/'),
      };
    }
    return metaData;
  }
  result.reverse();
  let isRest = false;

  const slug = /^\.\.\.(.*)$/.exec(result[1]);
  if (slug) {
    isRest = true;
    result[1] = slug[1];
  }

  return _calcRouteMetaData({
    ...metaData,
    urlPath: isRest
      ? `${result[2]}:${result[1]}(.*)`
      : `${result[2]}:${result[1]}${result[0]}`,
    slugs: [
      ...(metaData.slugs ?? []),
      {
        name: result[1],
        isRest,
      },
    ],
  });
}

function route2RouteConfig(route: MetaData, wrap: string) {
  return `
  ['${route.urlPath}']: ${wrap.replace('$1', route.component)}
`;
}

function route2RouteComponent(route: MetaData) {
  return `
<Route path='${route.urlPath}' component={RouteConfig["${route.urlPath}"]} exact />
`;
}

type GenCodeInput = Partial<{
  sourceHead: string;
  wrap: string;
  targetDir: string;
  ignorePatterns: string[];
  prefetch: boolean;
}>;
export function generate(params: GenCodeInput) {
  const {
    sourceHead = "import React from 'react'; import { Route } from 'react-router'; import loadable from '@loadable/component';",
    wrap = 'loadable($1)',
    targetDir = 'src/pages',
    ignorePatterns = [],
    prefetch = false,
  } = params;

  const routes = globSync(`${targetDir}/**/*`, ignorePatterns)
    .map((filePath) => {
      const result = /^src\/(.*)$/.exec(filePath);
      assertsHasValue(result, "There can't be an error here.");
      return `@/${result[1]}`;
    })
    .map((componentPath) => genRouteMetaData(componentPath, prefetch))
    .sort(compareFunc('urlPath'))
    .reverse();

  const typeCode = `export type UseParamsType = {
    ${routes
      .filter((route) => route.slugs)
      .map(
        (route) =>
          `['${route.path}']: { ${(route.slugs as Slugs)
            .map((slug) => `['${slug.name}']: string`)
            .join(';')} }`,
      )
      .join(';')}
  }`;

  const routesCode = `
  ${sourceHead}
  ${typeCode}
  export const RouteConfig = {
    ${routes.map((route) => route2RouteConfig(route, wrap))}
  }
  export default () => (
    <>
      ${routes.map((route) => route2RouteComponent(route)).join('')}
    </>
  )
  `;

  return {
    routesCode,
  };
}

ご静聴ありがとうございました

Made with Slides.com