はじめに
個人開発でReactを使用する機会が多いです。
ですが、毎回「どうやって作成してたっけ?」となるため、今後の個人開発円滑化のために備忘録として残しておきます。
コマンドやファイルなどを列挙する程度なので、説明などはあまりありません。
ご了承ください。
プロジェクト作成
下記コマンドを実行します。
docker run --rm -it -v $PWD:/app -w /app node:20-slim \
sh -c 'npm install -g npm && npm create -y vite@latest react-vite-sample -- --template react-swc-ts'
Dockerコンテナを作成し、viteプロジェクトを作成するコマンドを実行しています。
私の端末がAppleシリコンのMacのため--platform=linux/amd64
をつけています。
Docker Desktop 4.25から正式版となったRosetta for Linuxのおかげで、必要なくなりました。
react-vite-sample
の部分は任意のプロジェクト名に変更してください。
Docker関連ファイル作成
フォルダ階層
プロジェクトルート
├─ .devcontainer
| └─ devcontainer.json
├─ docker-compose.yml
└─ Dockerfile
docker-composeを使用するとスタック名が親ディレクトリの名称となります。
以前はdockerディレクトリにdocker-compose.ymlを配置していましたが、それだと他のプロジェクトとスタック名が重複してしまうため、プロジェクトルート直下に配置しています。
ファイル
{
"service": "react-vite-sample",
"dockerComposeFile": "../docker-compose.yml",
"workspaceFolder": "/workdir",
"customizations": {
"vscode": {
"extensions": ["eamodio.gitlens", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"],
"settings": {
"prettier.configPath": ".prettierrc.json",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.tabSize": 2
}
}
}
}
version: '3.9'
services:
react-vite-sample:
ports:
- 5173:5173
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/workdir
tty: true
FROM node:20-slim
RUN apt-get update && apt-get install -y git vim locales-all
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
WORKDIR /workdir
locales-allをインストールし、ENVを用いて環境変数を設定することで、コンテナ内でgitを使用する際に日本語入力時に文字化けしなくなります。
上記を配置し、下記コマンドでuser.emailとuser.nameを設定すればコンテナ内でコミットなどが可能になります。
git config --global user.email <メールアドレス>
git config --global user.name <名前>
また、ライブラリインストールのために以下コマンドも実行します。
npm install
ESlint, Prettier設定
フォルダ階層
プロジェクトルート
├─ .eslintrc.cjs
├─ .prettier.json
├─ tsconfig.json
└─ vite.config.ts
ファイル
下記コマンドを実行
npm install eslint prettier @typescript-eslint/eslint-plugin \
@typescript-eslint/parser eslint-config-prettier \
eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react \
eslint-import-resolver-typescript
.eslintrc.cjsを変更
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
+ 'plugin:import/recommended',
+ 'plugin:jsx-a11y/recommended',
+ 'eslint-config-prettier',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
+ settings: {
+ react: {
+ version: 'detect',
+ },
+ 'import/resolver': {
+ typescript: {},
+ },
+ },
rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
+ 'no-restricted-imports': ['error', { patterns: ['./', '../'] }],
},
};
.prettier.jsonをプロジェクトフォルダ直下に新規作成
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 120,
"bracketSpacing": true
}
srcディレクトリ配下を@/
の形でインポートできるようにします。
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
+ "baseUrl": "./",
+ "paths": {
+ "@/*": ["src/*"]
+ }
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
+ resolve: {
+ alias: {
+ '@': '/src',
+ },
+ },
});
エラーになるため、main.tsx, App.tsxを修正します。
import React from 'react';
import ReactDOM from 'react-dom/client';
- import App from './App.tsx';
+ import App from '@/App.tsx';
- import '@/index.css';
+ import '@/index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
import { useState } from 'react';
- import reactLogo from './assets/react.svg';
+ import reactLogo from '@/assets/react.svg';
+ // eslint-disable-next-line import/no-unresolved
import viteLogo from '/vite.svg';
- import './App.css';
+ import '@/App.css';
function App() {
const [count, setCount] = useState(0);
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
</>
);
}
export default App;
テスト準備
下記コマンドを実行
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom
srcディレクトリ直下にApp.test.tsxを作成します。
テストを一つ記述しているのみなので、必要に応じて追加、修正してください。
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { expect, test } from 'vitest';
import App from '@/App';
test('renders h1 text', () => {
render(<App />);
const headerElement = screen.getByText(/Vite \+ React/);
expect(headerElement).toBeInTheDocument();
});
vite.config.tsを下記のように変更します。
+ /// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': '/src',
},
},
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ },
});
package.jsonにscriptプロパティにtestを追加します。
{
"name": "test-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
+ "test": "vitest"
},
以下略...
}
CloudFormationテンプレート作成
CloudFront + S3でホスティングし、CodeCommitへのpush(この構成においてはGitHub ActionsでのCodeCommitへのミラーリング)をトリガーとしてCodePipelineを起動し、デプロイを行うテンプレートになります。
react-vite-sample
とReactViteSample
の文字列を任意のプロジェクト名に置換することで他のプロジェクトへ流用が簡単になると思います。
フォルダ構成
.
├─ cloudformation
│ ├─ parameters
│ │ ├─ codepipeline.json
│ │ ├─ hosting.json
│ │ └─ repository.json
│ ├─ shell
│ │ ├─ 01-repository.sh
│ │ ├─ 02-hosting.sh
│ │ ├─ 03-codebuild.sh
│ │ └─ 04-codepipeline.sh
│ └─ templates
│ ├─ codebuild.yml
│ ├─ codepipeline.yml
│ ├─ hosting.yml
│ └─ repository.yml
└─ buildspec.yml
parameters
{
"Parameters": {
"RepositoryName": "react-vite-sample"
}
}
{
"Parameters": {
"SourceBucketName": "react-vite-sample-source-bucket",
"DestributionName": "react-vite-sample-destribution"
}
}
{
"Parameters": {
"CodePipeLineName": "react-vite-sample-pipeline",
"ArtifactBucketName": "react-vite-sample-pipeline-artifact-store",
"EventBridgeName": "react-vite-sample-eventbridge-pipeline",
"EventBridgeRoleName": "react-vite-sample-eventbridge-role",
"EventBridgePolicyName": "react-vite-sample-eventbridge-policy"
}
}
shell
ファイル名に01などをつけていますが、CodePipelineが最後に実行されれば、それ以外は順不同です。
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
if [ $# = 1 ] && [ $1 = "deploy" ]; then
echo "deploy mode"
CHANGESET_OPTION=""
fi
CFN_TEMPLATE=../templates/repository.yml
CFN_STACK_NAME=react-vite-sample-repository
aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION}\
--parameter-overrides file://../parameters/repository.json
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
if [ $# = 1 ] && [ $1 = "deploy" ]; then
echo "deploy mode"
CHANGESET_OPTION=""
fi
CFN_TEMPLATE=../templates/hosting.yml
CFN_STACK_NAME=react-vite-sample-hosting
aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION}\
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides file://../parameters/hosting.json
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
if [ $# = 1 ] && [ $1 = "deploy" ]; then
echo "deploy mode"
CHANGESET_OPTION=""
fi
CFN_TEMPLATE=../templates/codebuild.yml
CFN_STACK_NAME=react-vite-sample-codebuild
aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION}\
--capabilities CAPABILITY_NAMED_IAM \
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
if [ $# = 1 ] && [ $1 = "deploy" ]; then
echo "deploy mode"
CHANGESET_OPTION=""
fi
CFN_TEMPLATE=../templates/codepipeline.yml
CFN_STACK_NAME=react-vite-sample-pipeline
aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION}\
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides file://../parameters/codepipeline.json
templates
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
RepositoryName:
Type: String
Resources:
CodeCommitRepository:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryName: !Ref RepositoryName
Outputs:
RepositoryName:
Value: !Ref RepositoryName
Export:
Name: ReactViteSampleRepositoryName
RepositoryArn:
Value: !GetAtt CodeCommitRepository.Arn
Export:
Name: ReactViteSampleRepositoryArn
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
SourceBucketName:
Type: String
DestributionName:
Type: String
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref SourceBucketName
VersioningConfiguration:
Status: Enabled
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: Allow CloudFront Access
Effect: Allow
Principal:
Service: cloudfront.amazonaws.com
Action: s3:GetObject
Resource: !Sub ${S3Bucket.Arn}/*
Condition:
StringEquals:
AWS:SourceArn: !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${Distribution}
Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- Id: S3Origin
DomainName: !GetAtt S3Bucket.DomainName
S3OriginConfig:
OriginAccessIdentity: ''
OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id
Enabled: true
DefaultRootObject: index.html
Comment: !Ref DestributionName
DefaultCacheBehavior:
TargetOriginId: S3Origin
CachePolicyId: !GetAtt CachePolicy.Id
ViewerProtocolPolicy: redirect-to-https
CloudFrontOriginAccessControl:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Description: TestProject Origin Access Control
Name: TestProjectOAC
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
CachePolicy:
Type: AWS::CloudFront::CachePolicy
Properties:
CachePolicyConfig:
DefaultTTL: 86400
MaxTTL: 31536000
MinTTL: 0
Name: TestProjectCachePolicy
ParametersInCacheKeyAndForwardedToOrigin:
CookiesConfig:
CookieBehavior: none
EnableAcceptEncodingGzip: false
HeadersConfig:
HeaderBehavior: none
QueryStringsConfig:
QueryStringBehavior: none
Outputs:
URL:
Value: !Sub https://${Distribution.DomainName}
SourceBucketName:
Value: !Ref SourceBucketName
Export:
Name: ReactViteSampleSourceBucketName
AWSTemplateFormatVersion: '2010-09-09'
Resources:
CodeBuild:
Type: AWS::CodeBuild::Project
Properties:
Artifacts:
Type: CODEPIPELINE
Environment:
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:4.0
Type: LINUX_CONTAINER
Name: test-project-frontend-build
ServiceRole: !GetAtt CodeBuildRole.Arn
Source:
BuildSpec: buildspec.yml
Type: CODEPIPELINE
CodeBuildRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Action: sts:AssumeRole
Path: /
Policies:
- PolicyName: CodeBuildPolicy
PolicyDocument:
Statement:
- Effect: Allow
Resource: '*'
Action:
- 's3:*'
- 'logs:*'
Outputs:
CodeBuild:
Value: !Ref CodeBuild
Export:
Name: ReactViteSampleCodeBuild
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
CodePipeLineName:
Type: String
ArtifactBucketName:
Type: String
EventBridgeName:
Type: String
EventBridgeRoleName:
Type: String
EventBridgePolicyName:
Type: String
Resources:
CodePipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: !Ref CodePipeLineName
RoleArn: !GetAtt PipelineRole.Arn
ArtifactStore:
Type: S3
Location: !Ref ArtifactStoreBucket
Stages:
- Name: Source
Actions:
- ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: 1
Configuration:
RepositoryName: !ImportValue ReactViteSampleRepositoryName
BranchName: main
PollForSourceChanges: false
Name: Source
OutputArtifacts:
- Name: SourceArtifact
RunOrder: 1
- Name: Build
Actions:
- ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: 1
Configuration:
ProjectName: !ImportValue ReactViteSampleCodeBuild
InputArtifacts:
- Name: SourceArtifact
Name: Build
OutputArtifacts:
- Name: BuildArtifact
RunOrder: 1
- Name: Deploy
Actions:
- ActionTypeId:
Category: Deploy
Owner: AWS
Provider: S3
Version: 1
Configuration:
BucketName: !ImportValue ReactViteSampleSourceBucketName
Extract: true
Name: Deploy
InputArtifacts:
- Name: BuildArtifact
RunOrder: 1
RestartExecutionOnUpdate: false
ArtifactStoreBucket:
Type: 'AWS::S3::Bucket'
Properties:
BucketName: !Ref ArtifactBucketName
LifecycleConfiguration:
Rules:
- Id: clear-old-objects-rule
Status: Enabled
ExpirationInDays: 3
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
PipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
Action: sts:AssumeRole
Path: /
Policies:
- PolicyName: CodePipelinePolicy
PolicyDocument:
Statement:
- Effect: Allow
Resource: '*'
Action:
- 's3:*'
- 'codecommit:*'
- 'codebuild:*'
EventBridge:
Type: AWS::Events::Rule
Properties:
Description: for codepipeline
EventPattern:
source:
- aws.codecommit
detail-type:
- 'CodeCommit Repository State Change'
resources:
- !ImportValue ReactViteSampleRepositoryArn
detail:
event:
- referenceCreated
- referenceUpdated
referenceType:
- branch
referenceName:
- main
Name: !Ref EventBridgeName
State: ENABLED
Targets:
- Arn: !Sub arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipeline}
Id: CodePipeline
RoleArn: !GetAtt EventBridgeIAMRole.Arn
EventBridgeIAMRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- events.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref EventBridgeIAMPolicy
RoleName: !Ref EventBridgeRoleName
EventBridgeIAMPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'codepipeline:StartPipelineExecution'
Resource:
- !Sub arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipeline}
ManagedPolicyName: !Ref EventBridgePolicyName
buildspec.yml
version: 0.2
phases:
install:
on-failure: ABORT
commands:
- if [ -e /tmp/node_modules.tar ]; then tar xf /tmp/node_modules.tar; fi
- npm install
pre_build:
on-failure: ABORT
commands:
- npm run test
build:
on-failure: ABORT
commands:
- npm run build
artifacts:
files:
- '**/*'
base-directory: dist
cache:
paths:
- /tmp/node_modules.tar
githubリポジトリ作成、ミラーリング用の設定
上記記事を参考に、下記を実施してください。
- SSHキーを作成
- CodeCommitの公開SSHキーとして登録
- GitHubリポジトリ作成
- 作成したリポジトリのActionsのSecretsを設定
githubリポジトリミラーリング用ファイル作成
CodePipelineを使うにあたりCodeCommitとの連携がやりやすいのですが、公開はできません。
リポジトリを公開できるようにGitHubリポジトリをCodeCommitへミラーリングします。
こうすることでCodePipelineはCodeCommitと連携させることができるので設定が楽になります。
CodeCommitリポジトリ作成
cloudformation/shell
ディレクトリへ移動し下記コマンドを実行します。
コンテナにはaws-cliをインストールしていないので、aws-cliをインストールしているホスト端末側で実行します。
sh 01-repository.sh deploy
上記コマンドでCodeCommitリポジトリが作成されるので、CodeCommitのページでSSHのURLをコピーします。
フォルダ階層
プロジェクトルート
└─ .github
└─ workflows
└─ main.yml
下記を作成します。target_repo_urlはコピーしたURLを貼り付けます。
name: Mirroring
on: [ push, delete ]
jobs:
to_codecommit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url: <コピーしたCodeCommitリポジトリのSSHのURL>
ssh_private_key: ${{ secrets.CODECOMMIT_SSH_PRIVATE_KEY }}
ssh_username: ${{ secrets.CODECOMMIT_SSH_PRIVATE_KEY_ID }}
サービス作成
cloudformation/shell
ディレクトリ配下のリポジトリを除いたシェルを順に実行します。
こちらもコンテナ内ではなく、aws-cliをインストールしたホスト端末で実行します。
sh 02-hosting.sh deploy
sh 03-codebuild.sh deploy
sh 04-codepipeline.sh deploy
デプロイ
githubリポジトリにコミットするとCodeCommitへ連携され、CodePipelineが起動して自動でデプロイがされます。
git init
git commit -m "first commit"
git remote add <作成したgithubリポジトリのURL>
git push -u origin main
デプロイ後はCloudFrontで対象のディストリビューションを選択し、ディストリビューションドメイン名をコピーしてURLに貼り付ければ画面の確認ができると思います。
反映まで時間がかかるので、エラーのような画面が表示される場合は時間をおいて確認してみてください。
おわりに
Amplifyを使えば簡単に構築できるかと思いますが、何をやっているのか分からないのが怖かったため、構築を行ってみました。
今後は何かを作る際に、この記事を見返して構築を完了させてから開発に取り掛かれるのは非常に嬉しいところです。
下記がGitHubリポジトリになります。
適当な場所にクローンして必要なフォルダを都度コピーして持ってくるのがいいかと思います。
参考記事
ESLint, Prettier
- Setting up ESLint & Prettier in ViteJS
- [import/no-unresolved] when using with typescript "baseUrl" and "paths" option #1485
CloudFormation
- CloudFormationの全てを味わいつくせ!「AWSの全てをコードで管理する方法〜その理想と現実〜」
- CloudFormation で OAI を使った CloudFront + S3 の静的コンテンツ配信インフラを作る
- CloudFormation】CloudFront の OAI を OAC に移行する
- AWS CodeBuildでnode_modulesをキャッシュしたらハマった
- CodePipelineをCloudFormationで作成してみた
- SPA(React)のビルドとデプロイを自動化するAWS CodePipeline用のCloudFormationテンプレート
Test
GitHubリポジトリミラーリング