【Vue.js】独自コンポーネントのタグとpropsをauto-completeしたい(願望)

大量にあるコンポーネントpropsを手で書くのは怠いので、どうにか自動で生成できないか？と調べに調べ、若干面倒ではありますが生成することが出来たので記事にしようと思います。
つまり、vetur用のhelper-json(tags.jsonとattributes.json)を自動生成する備忘録です。

# 自動生成したいコンポーネント群をまとめてライブラリ化する
## ビルド用スクリプトを記述する
コンポーネント群をまとめてライブラリ化するためのスクリプトを作成します。
自分は`src/build.components.js`としました。

```javascript:build.component.js
// require.contextで出力したいコンポーネント群を取得する
// サブディレクトリの検索をON
const files = require.context('出力したいコンポーネント群の相対パス', true, /.*\/vue$/);
let components = {};

// コンポーネント群をグローバル登録する時みたいな感じでcomponentsオブジェクトに格納していく
Object.values(files.keys()).forEach((key) => {
  const component = files(key).default;
  const name =
    component.name ||
    component
      .split("/")
      .pop()
      .replace(/\.\w+$/, "");
  components[name] = component;
});

// 格納したコンポーネント群のオブジェクトをexport
export default components;
```

## ビルドするために、package.jsonにnpmスクリプトを追記する
`package.json`にビルド用scriptを追記(scripts部分抜粋)

```json:package.json
scripts: {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "lint": "vue-cli-service lint",
  "build:components": "vue-cli-service build --target lib --name components ./src/build.components.js", // これを追記
}
```

`npm run build:components`を実行すると`dist`に以下画像のようにファイルが出力されます。
`vue.config.js`で`outputDir`を弄っている場合は各自の環境に合わせて読み替えてください。

![build.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230050/781e8de1-44cc-2f80-cac7-15709da7e1be.jpeg)

## tags.jsonとattributes.jsonを生成するためのスクリプトを作成する
ライブラリ化で出力されたjsからコンポーネント群を読み込み、`tags.json`と`attributes.json`を生成するためのスクリプトを作成します。
自分は`src/build.vetur.js`としました。

```javascript:build.vetur.js
const fs = require("fs");
const Vue = require("vue");
// .defaultを付与しないとexportされたオブジェクトを読み込めない
const files = require("../dist/components.common").default;

// 読み込んだコンポーネント群をVueオブジェクトに登録する
Object.values(files).forEach((key) => {
  const component = key;
  const name =
    component.name ||
    component
      .split("/")
      .pop()
      .replace(/\.\w+$/, "");

  Vue.component(name, component);
});

const hyphenateRE = /\B([A-Z])/g;

function hyphenate(str) {
  return str.replace(hyphenateRE, "-$1").toLowerCase();
}

function parseFunctionParams(func) {
  const groups = /function\s_.*\((.*)\)\s\{.*/i.exec(func);
  if (groups && groups.length > 1) return `(${groups[1]}) => {}`;
  else return "null";
}

function getPropType(type) {
  if (Array.isArray(type)) {
    return type.map((t) => getPropType(t));
  }

  if (!type) return "any";

  return type.name.toLowerCase();
}

function getPropDefault(def, type) {
  if (
    def === "" ||
    (def == null && type !== "boolean" && type !== "function")
  ) {
    return "undefined";
  } else if (typeof def === "function" && type !== "function") {
    def = def.call({});
  }

  if (type === "boolean") {
    return def ? "true" : "false";
  }

  if (type === "string") {
    return def ? `'${def}'` : def;
  }

  if (type === "function") {
    return parseFunctionParams(def);
  }

  return def;
}

function genProp(name, prop) {
  const type = getPropType(prop.type);

  return {
    name,
    type,
    default: getPropDefault(prop.default, type),
  };
}

function parseProps(component, array = []) {
  const options = component.options;
  const props = options.props || {};

  Object.keys(props).forEach((key) => {
    const generated = genProp(key, props[key], component.name);
    array.push(generated);
  });

  return array.sort((a, b) => a.name > b.name);
}

function writeJsonFile(obj, file) {
  const stream = fs.createWriteStream(file);

  stream.once("open", () => {
    stream.write(JSON.stringify(obj, null, 2));
    stream.end();
  });
}

const components = {};

// Vue.componentで登録したコンポーネント群が格納されている
const installedComponents = Vue.options._base.options.components;

// 不要なコンポーネントを除外する(何故か登録されている)
const excludes = ["KeepAlive", "Transition", "TransitionGroup"];

for (const name in installedComponents) {
  if (excludes.includes(name)) continue;

  const component = installedComponents[name];

  const kebabName = hyphenate(name);

  components[kebabName] = { props: parseProps(component) };
}

const tags = Object.keys(components).reduce((t, k) => {
  t[k] = {
    attributes: components[k].props
      .map((p) => p.name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`))
      .sort(),
    description: "",
  };

  return t;
}, {});

const attributes = Object.keys(components).reduce((attrs, k) => {
  const tmp = components[k].props.reduce((a, prop) => {
    let type = prop.type;

    if (!type) type = "";
    else if (Array.isArray(type))
      type = type.map((t) => t.toLowerCase()).join("|");
    else type = type.toLowerCase();

    const name = prop.name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`);

    a[`${k}/${name}`] = {
      type,
      description: "",
    };

    return a;
  }, {});

  return Object.assign(attrs, tmp);
}, {});

// 自分はsrc/utils/vetur下にtags.jsonとattributes.jsonを吐き出したいので
// src/utils/veturがない場合は作成するようにしています
if (!fs.existsSync("src/utils/vetur")) {
  fs.mkdirSync("src/utils/vetur", 0o755);
}

// tags.jsonとattributes.jsonを出力
writeJsonFile(tags, "src/utils/vetur/tags.json");
writeJsonFile(attributes, "src/utils/vetur/attributes.json");

console.log("tags.jsonとattributes.jsonが生成されました");
```

コンポーネントを読み込んだ後のjsonファイルを生成する部分はvuetify.jsの`api-generator`にあったビルドスクリプトをほぼパクってきて、自分用に要らない部分を削っています。

## package.jsonにnpmスクリプトを追記する
スクリプトの作成ができたので`package.json`にスクリプトを追記します

```json:package.json
scripts: {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "lint": "vue-cli-service lint",
  "build:api": "vue-cli-service build --target lib --name components ./src/build.components.js",
  "gen:api": "node ./src/build.vetur.js" // 追記
}
```

# json生成、package.jsonにvetur設定を追記する

veturにカスタムtags.jsonとattributes.jsonを読み込ませるための設定を記述します

```
  "vetur": {
    "tags": "src/utils/vetur/tags.json", // 自動生成させるtags.jsonとattributes.jsonのパスを記述
    "attributes": "src/utils/vetur/attributes.json"
  },
```

`npm run gen:api`を実行するとエラーが出なければ`src/utils/vetur`ディレクトリが作成され`tags.json`と`attributes.json`が出力されているかと思います。
VSCodeを再起動すれば、tagの補完とpropsの補完が有効になっているかと思います。

# 問題点
1つ問題として、`build.vetur.js`でVueC3を使ったコンポーネントを読み込むと`window is not defined`と出てしまいます。
出力されたjsを見るとSVG関連で使っている様で、node環境では`windowオブジェクト`が無いのでエラーが出るのも致し方無い。
もしこのようなエラーが出てしまう場合は、ライブラリ化するコンポーネントから除外してください。
現状、僕の知識では解決方法がわからないので、手動追記(スクリプト内に埋め込む)する形を取っています。(以下例)

```javascript:build.vetur.js
tags["graph"] = {
  attributes: ["type"],
  desctiption: "",
};

attributes["graph/type"] = {
  type: "string",
  description: "",
};
```

`tags,attributes`共に`const`ではなく`let`に変更するのをお忘れなく