LoginSignup
1
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-20

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

自動生成したいコンポーネント群をまとめてライブラリ化する

ビルド用スクリプトを記述する

コンポーネント群をまとめてライブラリ化するためのスクリプトを作成します。
自分はsrc/build.components.jsとしました。

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部分抜粋)

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.jsoutputDirを弄っている場合は各自の環境に合わせて読み替えてください。

build.jpg

tags.jsonとattributes.jsonを生成するためのスクリプトを作成する

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

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にスクリプトを追記します

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.jsonattributes.jsonが出力されているかと思います。
VSCodeを再起動すれば、tagの補完とpropsの補完が有効になっているかと思います。

問題点

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

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

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0