sapper-templateを使ってSvelteの開発環境を構築する

[Svelte](https://svelte.dev/)とても楽しいです。
こうしたらもっと楽だよとか、設定間違ってるよとかあればご指摘頂けると嬉しいです。

**SvelteKitが安定したらそちらに移行するのが良いと思います。**

# この記事でやること

* sapper-template導入
* Linter(ESLint)とFormatter(Prettier)導入
* Unit/Components Testing(Jest)導入
* E2E Testing(Cypress)導入
* git hook(husky)導入

# コード

[yoshida-san/sapper-ext: Svelte development environment](https://github.com/yoshida-san/sapper-ext)

# 参考記事

* [Docs Sapper](https://sapper.svelte.dev/docs)
* [VisualStudioCodeでSvelte3をESLintする設定メモ](https://qiita.com/masakurapa/items/54460660241761bed1f2)
* [sveltejs / prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte)
* [Multiple empty lines error when there are none in &lt;script&gt; tags](https://github.com/sveltejs/eslint-plugin-svelte3/issues/41)
* [Unable to add a space before function parens](https://github.com/prettier/prettier-vscode/issues/494)
* [Svelteでテストがしたい](https://qiita.com/nishinoshake/items/4d89eb44251059c613c3)

先人の知恵をお借りして進めていきます。いつものことながら先人には感謝しかないです。

# Sapperについて

[こちら](https://sapper.svelte.dev/)を参照ください。Svelteについては[こちら](https://svelte.dev/)を参照ください。
Sapperって名前、良いですよね。

# 環境構築

IDEはVSCodeを使っていきます。

```sh
~$ npx degit "sveltejs/sapper-template#rollup" sapper-sample
~$ cd sapper-sample
~$ npm i
~$ npm run dev
```

`localhost:3000`で確認して'GREATE SUCCESS!'が見えればOK。

## TypeScript support

**※一応記載しておきますが、今回はTSを導入せずに進めていきます。**
TSサポートは嬉しいですがLinterやFormatterも公式でサポートされる日が待ち遠しいですね。

```sh
~$ node scripts/setupTypeScript.js
~$ npm i
~$ npm run build
```

これで導入は完了ですが、そのままTypeScriptコードを書くとbuildでコケます。scriptタグにlang属性を追加する必要があります。src/routes/about.svelteで例を記します。

```html
<script lang='ts'>
    const title: string = 'About!!!'
</script>

<svelte:head>
    <title>{title}</title>
</svelte>

<h1>About this site</h1>

<p>This is the 'about' page. There's not much here.</p>
```

VSCodeプラグインのSvelte for VS Codeをインストールしておきましょう。

## eslint

```sh
~$ npm i -D eslint eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard eslint-plugin-svelte3 eslint-config-standard
```

`.eslintrc.js`

```js
module.exports = {
  parserOptions: {
    ecmaVersion: 2019,
    sourceType: 'module'
  },
  env: {
    es6: true,
    browser: true,
    node: true
  },
  extends: [
    'standard'
  ],
  plugins: [
    'svelte3'
  ],
  ignorePatterns: [
    '/node_modules/',
    '/__sapper__/',
    '/src/node_modules/@sapper/'
  ],
  overrides: [
    {
      files: ['**/*.svelte'],
      processor: 'svelte3/svelte3'
    }
  ],
  rules: {},
  settings: {}
}
```

以下のコマンドで動作確認（sapper-templateがそのままならそれなりの量がエラーになるはず）。

```sh
npx eslint --ext svelte,js src/
```

standardそのままで利用するとscriptタグのラインでno-multiple-empty-linesのエラーが発生してしまいます。.eslintrc.jsのrulesで対応していきます。

```js
'no-multiple-empty-lines': [
    'error',
    {
        max: 2,
        maxBOF: 2,
        maxEOF: 0
    }
]
```

npm scriptに追加しておきましょう。`--fix`で整形もできますが整形はPrettierに任せます（ESLintに任せられる範囲で任せる場合は--fixで良いと思います）。

```js
"lint": "eslint --ext svelte,js src/"
```

## prettier

```sh
~$ npm i -D prettier-plugin-svelte prettier
```

`.prettierrc.js`
（あえてフォーマットをぐちゃぐちゃにしてます）

```js
module.exports = {
    svelteSortOrder : "options-scripts-markup-styles",
    svelteStrictMode: false,
  svelteBracketNewLine: true,
          svelteAllowShorthand: true,
  singleQuote: true,
  trailingComma: "none",
    tabWidth: 2,
  semi: false
}
```

以下のコマンドで動作確認。

```sh
npx prettier .prettierrc.js
```

整形後のコードが表示されていればOKです。`--write`オプションを付けて実行すればコードが自動で整形されます。

続いて以下のコマンドで整形します。

```sh
npx prettier --write 'src/**/*.{js,svelte}'
```

eslintのspace-before-function-parenでエラーが発生します。しかしながらeslintのspace-before-function-parenに該当する設定がprettierには無く、ここが問題となってしまいます。ここではrulesを追加することで対応していきます。

```js
'space-before-function-paren': ['error', 'never']
```

JavaScript Standard Styleからズレていくのはあまり良いとは言えませんがやむ無し...。rulesの細かい調整等は開発チームのコーディング規約に準じて修正してください。

最後にnpm scriptに追加しておきましょう。

```js
"format": "prettier --write 'src/**/*.{js,svelte}'"
```

## VSCode Extension

* [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
* [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
* [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode)

ファイル保存時にフォーマットをかける場合は、.vscode/setting.jsonに以下を追加します（またはPreferences->Settingsから設定します）。

```js
"editor.fomatOnSave": true
```

## Sample Code

テスト用にコードを用意します。
src/routes配下にcounterディレクトリを作成します。counterディレクトリ内に[Svelte: Examples - Custom Stores](https://svelte.dev/examples#custom-stores)をベースにしたコードを記述したファイルを作成します。

`src/routes/counter/index.svelte`

```js
<script>
  import { count } from './store.js'
</script>

<h1>The count is <span data-test="result">{$count}</span></h1>

<button data-test="increment" on:click={count.increment}>+</button>
<button data-test="decrement" on:click={count.decrement}>-</button>
<button data-test="reset" on:click={count.reset}>reset</button>
```

`src/routes/counter/store.js`

```js
import { writable } from 'svelte/store'

function createCount() {
  const { subscribe, set, update } = writable(0)

  return {
    subscribe,
    increment: () => update((n) => n + 1),
    decrement: () => update((n) => n - 1),
    reset: () => set(0),
    set: (n) => set(n)
  }
}

export const count = createCount()

```

ファイルの作成が完了したら、src/components/Nav.svelteにリンクを追加。

```html
<li>
  <a aria-current={segment === "counter" ? "page" : undefined} href="counter">
    counter
  </a>
</li>
```

## Unit testing

### Ready

```sh
~$ npm i -D jest babel-jest @babel/core @babel/preset-env
```

`babel.config.js`

```js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          chrome: '87',
          firefox: '82',
          safari: '14',
          node: 'current'
        }
      }
    ]
  ]
}
```

`jest.config.js`

```js
module.exports = {
  verbose: true,
  transform: {
    "^.+\\.js$": "babel-jest"
  }
}
```

`.eslintrc.js`のenvに以下を追加。

```js
env: {
  :
  'jest/globals': true
}
```

### Testing

`test/unit/counter/store.test.js`

```js
import { get } from 'svelte/store'
import { count } from '../../../src/routes/counter/store.js'

describe('Testing counter/store', () => {
  it('Increment(Positive number)', () => {
    count.set(0)
    expect(get(count)).toBe(0)
    count.increment()
    expect(get(count)).toBe(1)
  })

  it('Increment(Negative number)', () => {
    count.set(-100)
    expect(get(count)).toBe(-100)
    count.increment()
    expect(get(count)).toBe(-99)
  })

  it('Decrement(Positive number)', () => {
    count.set(100)
    expect(get(count)).toBe(100)
    count.decrement()
    expect(get(count)).toBe(99)
  })

  it('Decrement(Negative number)', () => {
    count.set(-99)
    expect(get(count)).toBe(-99)
    count.decrement()
    expect(get(count)).toBe(-100)
  })
})
```

以下のコマンドでUnit unit testを実行します。

```sh
~$ npx jest test/unit/
PASS  test/unit/counter/store.test.js
 Testing counter/store
   ✓ Increment(Positive number) (2 ms)
   ✓ Increment(Negative number) (1 ms)
   ✓ Decrement(Positive number)
   ✓ Decrement(Negative number)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        2.938 s
```

npm scriptに追加しておきます。

```js
"test:unit": "jest test/unit"
```

## Components testing

### Ready

```sh
~$ npm i -D @testing-library/svelte jest-transform-svelte
```

`jest.config.js`

```js
module.exports = {
  verbose: true,
  transform: {
    '^.+\\.js$': 'babel-jest',
    '^.+\\.svelte$': 'jest-transform-svelte'
  },
  moduleFileExtensions: ['js', 'svelte']
}
```

### Testing

`test/components/counter/counter.test.js`

```js
import { render, fireEvent } from '@testing-library/svelte'
import Counter from '../../../src/routes/counter/index.svelte'

it('Testting counter component', async () => {
  const { container } = render(Counter)
  const incrementButton = container.querySelector(
    'button[data-test="increment"]'
  )
  const resultText = container.querySelector('span[data-test="result"]')

  await fireEvent.click(incrementButton)
  expect(resultText.textContent).toBe('1')
})
```

以下のコマンドでComponent testを実行します。

```sh
~$ npx jest test/components/
PASS  test/components/counter/counter.test.js
 ✓ Testting counter component (28 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.764 s
```

npm scriptに追加しておきます。

```js
"test:components": "jest test/components"
```

## E2E testing

### Ready

```sh
~$ npm i -D cypress
```

`cypress.json`

```js
{
  "video": false,
  "baseUrl": "http://localhost:3000",
  "fixturesFolder": "test/e2e/fixtures",
  "integrationFolder": "test/e2e/integration",
  "screenshotsFolder": "test/e2e/screenshots",
  "videosFolder": "test/e2e/videos",
  "pluginsFile": false,
  "supportFile": false
}
```

### Testing

`test/e2e/integration/counter.test.js`

```js
it('Counter increment', () => {
  cy.visit('/counter')
  cy.get('span').should('have.text', '0')
  cy.get('button[data-test="increment"]').should('have.text', '+')
  cy.get('button[data-test="increment"]').click()
  cy.get('span[data-test="result"]').should('have.text', '1')
})

it('Counter decrement', () => {
  cy.visit('/counter')
  cy.get('span').should('have.text', '0')
  cy.get('button[data-test="decrement"]').should('have.text', '-')
  cy.get('button[data-test="decrement"]').click()
  cy.get('span[data-test="result"]').should('have.text', '-1')
})
```

テスト前にビルド&ローカルサーバ起動を行います。

```sh
~$ npm run build
~$ npm run start
```

cypressを走らせます（またはopenで起動してアプリケーション内でテストを実行します）。

```sh
~$ npx cypress run
or
~$ npx cypress open
```

## Git hooks

### Ready

```sh
~$ npm i -D husky
```

lint-stagedは使っていませんが、必要に応じてlint-stagedを併用するのが良いと思います。

### Hooks settings

commitのタイミングでlint && formatを走らせて、pushのタイミングでtest:unit && test:componentsを走らせます。

`package.json`

```js
"husky": {
  "hooks": {
    "pre-commit": "npm run lint && npm run format",
    "pre-push": "npm run test:unit && npm run test:components"
  }
}
```

# おわり

環境周りはまだまだ変化していくと思うので、半年後には役に立たない記事になっていそうな気がします。LintやFormatter、Testing等もSapperに含まれたらいいですね。

Cypressを初めて使いましたが学習コスト低くて良いですね。導入も簡単です。