PdfMakeを使ったPDF作成APIをZeit Nowで動かしたメモ

## 概要
PDFをサーバサイドで作成し、ダウンロードするAPIを作成した。
ローカルで動いていたものがデプロイ時に動かないなどのトラブルがあったが、
最終的には成功できた。

[作成したソース](https://github.com/hibohiboo/develop/tree/50f65f9a5d578a4630bd69ca4c2bb5aa5a7ebdea/tutorial/lesson/react-my-examples/scenario-sheet/)

## ローカル環境
* node:13.12.0
* Now CLI 17.1.1

## フォルダ構成

```
- api
  - pdf.ts
- fonts
  - ipaexg.ttf
  - ipaexm.ttf
- src
  - pages
    - index.tsx
    - PdfArea.tsx
- .babelrc.js
- next-env.d.ts
- next.config.js
- now.json
- package.json
- tsconfig.json
```

## ハマった点

APIを`src/pages/api/pdf.ts`に書いていたとき、ローカルで動かすときには成功し、デプロイしたときにエラーとなる現象が発生した。
デプロイ時には`fonts`フォルダが見えなくてファイルが開けない問題が発生していた。
apiはソース通りのフォルダ構成ではなく、ラムダ環境にコンパイル済で配置されていたことが原因と考えられる。
apiファイルの場所を、`src/pages/api`から直下の`api`に移し、now.jsonに`incluceFiles`を追加することで解決した。

```json:now.json
{
  "version": 2,
  "functions": {
    "api/pdf.ts": {
      "includeFiles": "fonts/**"
    }
  }
}
```

`incluceFiles`を使うときは、nextの仕組みの`pages/api`は使えない模様。
以下では、コンパイルは通るが、フォルダがアップロードされなかった。

```json:now.jsonのうまくいかなかった例
{
  "version": 2,
  "functions": {
    "src/pages/api/pdf.ts": {
      "includeFiles": "fonts/**"
    }
  }
}
```

## ファイル

```tsx:src/pages/index.tsx
import Head from 'next/head'
import * as React from 'react'
import { NextPage } from 'next'
import PdfArea from './PdfArea'

const Home: NextPage = () => {
  return (
    <div className="container">
      <main>
        <PdfArea />
      </main>
    </div>
  )
}

export default Home
```

```tsx:src/pages/PdfArea.tsx
import React, { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import scenarioModule, {
  useScenario,
  usePdf
} from '../store/modules/scenarioModule'

const makePdf = async (scenario, dispatch) => {
  const pdf = await (
    await fetch('/api/pdf', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json; charset=utf-8' },
      body: JSON.stringify(scenario)
    })
  ).text()
  dispatch(scenarioModule.actions.setPdf(pdf))
}

const PdfArea: React.FC = () => {
  const scenario = useScenario()
  const pdf = usePdf()
  const dispatch = useDispatch()

  if (!scenario) {
    return <div>読込失敗</div>
  }
  return (
    <>
      <button onClick={(e) => makePdf(scenario, dispatch)}>PDFを作る</button>
      {pdf !== '' && (
        <a href={pdf} download="scenario.pdf">
          作成したPDFをダウンロード
        </a>
      )}
    </>
  )
}

export default PdfArea
```

```ts:api/pdf.ts
import { NextApiRequest, NextApiResponse } from 'next'
import path from 'path'
import PdfPrinter from 'pdfmake'
import { Scenario } from '../src/store/modules/scenarioModule'

function createPdfBinary(pdfDoc, callback) {
  const baseDir = 'fonts/'
  const gPath = path.resolve(baseDir + 'ipaexg.ttf')
  const mPath = path.resolve(baseDir + 'ipaexm.ttf')
  const fontDescriptors = {
    IPASerif: {
      normal: mPath,
      bold: mPath,
      italics: mPath,
      bolditalics: mPath
    },
    IPAGothic: {
      normal: gPath,
      bold: gPath,
      italics: gPath,
      bolditalics: gPath
    }
  }
  const printer = new PdfPrinter(fontDescriptors)
  const doc = printer.createPdfKitDocument(pdfDoc)
  const chunks = []
  doc.on('data', function(chunk) {
    chunks.push(chunk)
  })
  doc.on('end', function() {
    const result = Buffer.concat(chunks)
    callback('data:application/pdf;base64,' + result.toString('base64'))
  })
  doc.end()
}

export default (req: NextApiRequest, res: NextApiResponse) => {
  const scenario: Scenario = req.body
  const docDefinition = {
    content: [
      { text: scenario.title, fontSize: 55, font: 'IPASerif' },
    ],
    defaultStyle: {
      font: 'IPAGothic',
      alignment: 'center'
    }
  }
  createPdfBinary(docDefinition, (binary) => {
    res.setHeader('Content-Type', 'application/json')
    res.status(200).send(binary)
  })
}
```


```json:tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./src/*"
      ],
    },
    "target": "es6",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ]
}
```

```js:next.config.js
const { resolve } = require('path')
const withOffline = require('next-offline')

const nextConfig = {
  webpack: (config) => {
    // src ディレクトリをエイリアスのルートに設定
    config.resolve.alias['~'] = resolve(__dirname, 'src')
    return config
  },
  // manifest設定
  target: 'serverless',
  transformManifest: (manifest) => ['/'].concat(manifest),
  generateInDevMode: true,
  workboxOpts: {
    swDest: 'static/service-worker.js',
    runtimeCaching: [
      {
        urlPattern: /^https?.*/,
        handler: 'NetworkFirst',
        options: {
          cacheName: 'https-calls',
          networkTimeoutSeconds: 15,
          expiration: {
            maxEntries: 150,
            maxAgeSeconds: 30 * 24 * 60 * 60 // 1 month
          },
          cacheableResponse: {
            statuses: [0, 200]
          }
        }
      }
    ]
  }
}

// PWA に対応
module.exports = withOffline(nextConfig)
```

```js:.babelrc.js
// ローカルの開発サーバー側の SSR 時と クライアント側のCSR 時に styled-components が付与するクラス名に差が生まれるエラーの対応
module.exports = {
  presets: ['next/babel'],
  plugins: [
    ['styled-components', { ssr: true, displayName: true, preprocess: false }]
  ]
}
```


## 参考

[including-additional-files](https://zeit.co/docs/runtimes#advanced-usage/technical-details/including-additional-files)
[pdfmake](https://pdfmake.github.io/docs/getting-started/server-side/)
[IPAex フォント ダウンロードページ](https://ipafont.ipa.go.jp/old/ipaexfont/download.html)
[IPAex フォント ダウンロードページ](https://ipafont.ipa.go.jp/old/ipaexfont/download.html)
[Zeit Now の具体的な Tips 集](https://qiita.com/mt0m/items/3e58d6185a5335729ccc)
[Javascript で PDF を作成する](https://qiita.com/shuhei_sakiyama/items/ef68d49b7199d50a853b)