🙏

祈ってコンポーネントを生成するVSCode拡張機能"PrayCode"を作った

2023/06/04に公開

それ、書くより祈った方が早くないですか?

・そこまでクリティカルじゃないロジック
・ぐぐってコピペして魔改造するようなカスタムフック
・どこにでもありそうなコンポーネント
・プロトタイプのために生成されたコード

そんなコードは祈って生成してもらいましょう🙏

概要

・指定のディレクトリにコンポーネントファイルを自動生成できるVSCode拡張機能を開発した
・コンポーネント名、概要、Props、使用するライブラリなどを指定できる
既存のコードを読み込まないので、漏洩等を気にせず使える
・ReactとVueに対応(.tsx, .jsx, .vue)

出力コード例

React,TypeScript,CSS Modules
// Version of imported packages:
// React: 17.0.2
// CSS Modules: 4.2.2
// TypeScript: 4.3.5

import React, { useState } from 'react';
import styles from './ArticleEditForm.module.css';

interface Props {
  title: string;
  description: string;
  tag?: string[];
}

const ArticleEditForm: React.FC<Props> = ({ title, description, tag }) => {
  const [newTitle, setNewTitle] = useState(title);
  const [newDescription, setNewDescription] = useState(description);
  const [newTag, setNewTag] = useState(tag || []);

  const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setNewTitle(event.target.value);
  };

  const handleDescriptionChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
    setNewDescription(event.target.value);
  };

  const handleTagChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const newTagValue = event.target.value;
    if (newTagValue.trim() !== '') {
      setNewTag([...newTag, newTagValue]);
      event.target.value = '';
    }
  };

  const handleTagDelete = (index: number) => {
    const newTagList = [...newTag];
    newTagList.splice(index, 1);
    setNewTag(newTagList);
  };

  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    // Submit form data to backend
  };

  return (
    <form className={styles.form} onSubmit={handleSubmit}>
      <label htmlFor="title">Title:</label>
      <input type="text" id="title" value={newTitle} onChange={handleTitleChange} required />

      <label htmlFor="description">Description:</label>
      <textarea id="description" value={newDescription} onChange={handleDescriptionChange} required />

      <label htmlFor="tag">Tag:</label>
      <div className={styles.tagContainer}>
        {newTag.map((tag, index) => (
          <div key={index} className={styles.tag}>
            {tag}
            <button type="button" onClick={() => handleTagDelete(index)}>
              X
            </button>
          </div>
        ))}
      </div>
      <input type="text" id="tag" placeholder="Add tag" onChange={handleTagChange} />

      <button type="submit">Save</button>
    </form>
  );
};

export default ArticleEditForm;
React,TypeScipt,TailWind CSS
// Version of imported packages:
// React: 17.0.2
// Tailwind CSS: 2.2.7
// TypeScript: 4.4.3

import React, { useState } from 'react';

interface Props {
  title: string;
  description: string;
  tag?: string[];
}

const ArticleEditForm: React.FC<Props> = ({ title, description, tag }) => {
  const [newTitle, setNewTitle] = useState(title);
  const [newDescription, setNewDescription] = useState(description);
  const [newTag, setNewTag] = useState(tag || []);

  const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setNewTitle(event.target.value);
  };

  const handleDescriptionChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
    setNewDescription(event.target.value);
  };

  const handleTagChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const newTagValue = event.target.value;
    if (newTagValue.trim() !== '') {
      setNewTag([...newTag, newTagValue]);
      event.target.value = '';
    }
  };

  const handleTagDelete = (index: number) => {
    const newTagList = [...newTag];
    newTagList.splice(index, 1);
    setNewTag(newTagList);
  };

  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    // Submit form data to backend
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="title">Title:</label>
      <input type="text" id="title" value={newTitle} onChange={handleTitleChange} required />

      <label htmlFor="description">Description:</label>
      <textarea id="description" value={newDescription} onChange={handleDescriptionChange} required />

      <label htmlFor="tag">Tag:</label>
      <div className="flex flex-wrap">
        {newTag.map((tag, index) => (
          <div key={index} className="bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2 flex items-center">
            {tag}
            <button type="button" className="ml-2" onClick={() => handleTagDelete(index)}>
              <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
                <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L10 8.586l-2.293-2.293a1 1 0 00-1.414 1.414L8.586 10l-2.293 2.293a1 1 0 001.414 1.414L10 11.414l2.293 2.293a1 1 0 001.414-1.414L11.414 10l2.293-2.293z" clipRule="evenodd" />
              </svg>
            </button>
          </div>
        ))}
        <input type="text" id="tag" className="bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2" placeholder="Add tag" onChange={handleTagChange} />
      </div>

      <button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mt-4">
        Save
      </button>
    </form>
  );
};

export default ArticleEditForm;
Vue,TypeScript,TailWind CSS
// Version of imported packages:
// Vue 3.2.6
// Tailwind CSS 2.2.16
// TypeScript 4.4.4

<template>
  <form class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
    <div class="mb-4">
      <label class="block text-gray-700 font-bold mb-2" for="title">
        Title
      </label>
      <input
        v-model="title"
        class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
        id="title"
        type="text"
        placeholder="Enter title"
        required
      />
    </div>
    <div class="mb-4">
      <label class="block text-gray-700 font-bold mb-2" for="description">
        Description
      </label>
      <textarea
        v-model="description"
        class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
        id="description"
        placeholder="Enter description"
        required
      ></textarea>
    </div>
    <div class="mb-4">
      <label class="block text-gray-700 font-bold mb-2" for="tag">
        Tag
      </label>
      <input
        v-model="tag"
        class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
        id="tag"
        type="text"
        placeholder="Enter tag"
      />
    </div>
    <div class="flex items-center justify-between">
      <button
        class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
        type="submit"
        @click.prevent="submitForm"
      >
        Submit
      </button>
    </div>
  </form>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'ArticleEditForm',
  props: {
    title: {
      type: String,
      required: true,
    },
    description: {
      type: String,
      required: true,
    },
    tag: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      title: this.title,
      description: this.description,
      tag: this.tag,
    };
  },
  methods: {
    submitForm() {
      const formData = {
        title: this.title,
        description: this.description,
        tag: this.tag,
      };
      console.log(formData);
      // Submit form data to backend API
    },
  },
});
</script>

<style>
/* Tailwind CSS styles */
</style>
React,TypeScipt,TailWind CSS,React-hook-form,React Query
// Version of imported packages:
// React: 17.0.2
// React-hook-form: 7.16.0
// React Query: 3.16.0
// Tailwind CSS: 2.2.7
// TypeScript: 4.4.3

import { useForm } from 'react-hook-form';
import { useMutation } from 'react-query';

interface ArticleEditFormProps {
  title: string;
  description: string;
  tag?: string[];
}

export default function ArticleEditForm(props: ArticleEditFormProps) {
  const { title, description, tag } = props;

  const { register, handleSubmit, formState: { errors } } = useForm();

  const { mutate, isLoading, isError, isSuccess } = useMutation(
    (data: ArticleEditFormProps) => {
      // API call to update article
    }
  );

  const onSubmit = (data: ArticleEditFormProps) => {
    mutate(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="title">Title</label>
        <input
          type="text"
          id="title"
          defaultValue={title}
          {...register("title", { required: true })}
        />
        {errors.title && <span>This field is required</span>}
      </div>
      <div>
        <label htmlFor="description">Description</label>
        <textarea
          id="description"
          defaultValue={description}
          {...register("description", { required: true })}
        />
        {errors.description && <span>This field is required</span>}
      </div>
      <div>
        <label htmlFor="tag">Tag</label>
        <input
          type="text"
          id="tag"
          defaultValue={tag?.join(", ")}
          {...register("tag")}
        />
      </div>
      <button type="submit" disabled={isLoading}>
        {isLoading ? "Loading..." : "Submit"}
      </button>
      {isError && <span>Failed to update article</span>}
      {isSuccess && <span>Article updated successfully</span>}
    </form>
  );
}
Vue,TypeScript,Vuetify
// Version of imported packages:
// React-hook-form: 7.14.2
// React Query: 3.16.0
// Vue: 3.2.6
// Vuetify: 2.5.8
// TypeScript: 4.4.4

<template>
  <form @submit.prevent="handleSubmit">
    <v-text-field
      v-model="title"
      label="Title"
      :rules="[requiredRule]"
    ></v-text-field>
    <v-textarea
      v-model="description"
      label="Description"
      :rules="[requiredRule]"
    ></v-textarea>
    <v-select
      v-model="tag"
      label="Tag"
      :items="tagOptions"
      multiple
      chips
    ></v-select>
    <v-btn type="submit" color="primary">Submit</v-btn>
  </form>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { useForm } from 'react-hook-form';
import { useMutation } from 'react-query';

export default defineComponent({
  name: 'ArticleEditForm',
  props: {
    title: {
      type: String,
      required: true,
    },
    description: {
      type: String,
      required: true,
    },
    tag: {
      type: Array,
      default: () => [],
    },
  },
  setup(props) {
    const { register, handleSubmit } = useForm({
      defaultValues: {
        title: props.title,
        description: props.description,
        tag: props.tag,
      },
    });

    const { mutate } = useMutation((data) => {
      // API call to update article
      return fetch('/api/articles/' + articleId, {
        method: 'PUT',
        body: JSON.stringify(data),
      }).then((res) => res.json());
    });

    const requiredRule = (v: string) => !!v || 'Required';

    const tagOptions = ['tag1', 'tag2', 'tag3'];

    const onSubmit = (data: any) => {
      mutate(data);
    };

    return {
      register,
      handleSubmit,
      requiredRule,
      tagOptions,
      onSubmit,
    };
  },
});
</script>
React カスタムフック
// Version of imported packages:
// React: 17.0.2
// React-hook-form: 7.16.6
// React Query: 3.16.0
// Tailwind CSS: 2.2.7
// TypeScript: 4.4.3

import { useForm } from 'react-hook-form';
import { useMutation, useQueryClient } from 'react-query';

interface ArticleFormValues {
  title: string;
  content: string;
}

interface Article {
  id: number;
  title: string;
  content: string;
}

function useArticleForm(initialValues: ArticleFormValues, articleId?: number) {
  const { register, handleSubmit, setValue } = useForm<ArticleFormValues>({
    defaultValues: initialValues,
  });
  const queryClient = useQueryClient();

  const { mutate: createArticle } = useMutation((article: ArticleFormValues) =>
    fetch('/api/articles', {
      method: 'POST',
      body: JSON.stringify(article),
    }).then((res) => res.json())
  );

  const { mutate: updateArticle } = useMutation((article: Article) =>
    fetch(`/api/articles/${article.id}`, {
      method: 'PUT',
      body: JSON.stringify(article),
    }).then((res) => res.json())
  );

  const onSubmit = handleSubmit((data) => {
    if (articleId) {
      updateArticle({ id: articleId, ...data });
    } else {
      createArticle(data);
    }
  });

  const setFormValues = (article: Article) => {
    setValue('title', article.title);
    setValue('content', article.content);
  };

  const deleteArticle = async (id: number) => {
    await fetch(`/api/articles/${id}`, {
      method: 'DELETE',
    });
    queryClient.invalidateQueries('articles');
  };

  return {
    register,
    onSubmit,
    setFormValues,
    deleteArticle,
  };
}

export default useArticleForm;

インストール方法

https://marketplace.visualstudio.com/items?itemName=tesla0225.PrayCode
からInstall
もしくは
https://github.com/tesla0225/PrayCodes
からPackage化して使ってください

使い方

・OpenAIのAPIキーを取得、設定から入力
・デフォルトはGPT-3.5-Turboだが、GPT-4などにも変更可能

・React or Vue、CSSフレームワークなどを選択

・コンポーネント名、コンポーネントの概要を入力
・必要であればPropsも入力(より具体的なコードになります)
・必要であればライブラリ名を入力(カンマ区切りで複数入力可能)

・保存先ディレクトリを指定(今開いているワークスペースのルートディレクトリからの相対パス)

どんな人に使って欲しいか

・気軽にAIの恩恵を受けたい人
・モダンフロントエンド初学者
・設計は早いけどタイピングが遅い人
・フロントエンドをバックエンドの合間に行う人
・祈りたい人

今後の展開

HTMLと簡単なスクリプト程度の他言語も祈って生成できるようにアップデート予定
(コードにはありますがまだクオリティ等の問題で非表示にしています)

感想など

Github Copilotが費用の面や機能の面で導入できない開発者向けに作成しました。
今回の拡張機能はEmbedding等は使用していなくPromptだけで作成しているのですが、入力の労力に比べて良い出力になっていると思います。
VSCode拡張機能とReactを組み合わせて実装するのに苦労したのでそちらはまた別記事に...

参考になったらいいねとコメントなどでフィードバックいただけると嬉しいです!

Discussion