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


Svelteとても楽しいです。
こうしたらもっと楽だよとか、設定間違ってるよとかあればご指摘頂けると嬉しいです。

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

この記事でやること

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

コード

yoshida-san/sapper-ext: Svelte development environment

参考記事

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

Sapperについて

こちらを参照ください。Svelteについてはこちらを参照ください。
Sapperって名前、良いですよね。

環境構築

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

~$ 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も公式でサポートされる日が待ち遠しいですね。

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

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

<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

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

.eslintrc.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がそのままならそれなりの量がエラーになるはず)。

npx eslint --ext svelte,js src/

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

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

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

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

prettier

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

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

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

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

npx prettier .prettierrc.js

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

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

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

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

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

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

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

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

VSCode Extension

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

"editor.fomatOnSave": true

Sample Code

テスト用にコードを用意します。
src/routes配下にcounterディレクトリを作成します。counterディレクトリ内にSvelte: Examples - Custom Storesをベースにしたコードを記述したファイルを作成します。

src/routes/counter/index.svelte

<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

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にリンクを追加。

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

Unit testing

Ready

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

babel.config.js

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

jest.config.js

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

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

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

Testing

test/unit/counter/store.test.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を実行します。

~$ 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に追加しておきます。

"test:unit": "jest test/unit"

Components testing

Ready

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

jest.config.js

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

Testing

test/components/counter/counter.test.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を実行します。

~$ 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に追加しておきます。

"test:components": "jest test/components"

E2E testing

Ready

~$ npm i -D cypress

cypress.json

{
  "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

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')
})

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

~$ npm run build
~$ npm run start

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

~$ npx cypress run
or
~$ npx cypress open

Git hooks

Ready

~$ npm i -D husky

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

Hooks settings

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

package.json

"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を初めて使いましたが学習コスト低くて良いですね。導入も簡単です。