🗺

Next.jsが生成するSourceMapにgetServerSidePropsのコードが含まれる問題への対処

2022/02/23に公開

Next.jsが生成するSourceMapには、 pages/**/*.tsx のコードが全て含まれます。getServerSidePropsのコードも含まれます。
フロント側のコードのSourceMapは公開されて良いとしても、getServerSidePropsのコードは公開したくないわけです。

これを実現する方法として「getServerSidePropsのコードを pages/ 配下のファイルに記述しない」ことを提案します。

課題感

開発環境は当たり前として、本番環境でもSourceMapを公開したいケースがあります。
例えば、BugsnagやSentry等でStacktraceを見やすくし、トラブルシュートの効率を上げたいときです。

しかし、Next.jsが生成するSourceMapにはgetServerSidePropsのコードまで含まれてしまいます。つまり、サーバー側のロジックを公開してしまうことになるわけです。

SourceMap生成結果を見てみる

getServerSidePropsのコードがSourceMapに含まれてしまうことを見てみましょう。

まず画面を1つ用意します。

pages/hello.tsx
import type { GetServerSideProps, NextPage } from 'next';

const Hello: NextPage<ServerSideProps> = ({ message }) => {
  return <p>{message}</p>;
}

export default Hello;

type ServerSideProps = {
  message: string;
}

export const getServerSideProps: GetServerSideProps<ServerSideProps> = async () => {
  return {
    props: {
      message: "Hello World",
    },
  };
};

次に、SourceMapを生成するように設定を変更します。Next.jsはデフォルトではSourceMapを生成しません。

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  productionBrowserSourceMaps: true,
}

module.exports = nextConfig

そしてビルドを行い、生成されたSourceMapの中身を見てみます。
すると、getServerSidePropsのコードも含まれていることがわかります。

$ yarn build
...(snip)...

$ jq -r '.sourcesContent[]' .next/static/chunks/pages/hello-477f6a1428c2c431.js.map

    (window.__NEXT_P = window.__NEXT_P || []).push([
      "/hello",
      function () {
        return require("private-next-pages/hello.tsx");
      }
    ]);
    if(module.hot) {
      module.hot.dispose(function () {
        window.__NEXT_P.push(["/hello"])
      });
    }

import type { GetServerSideProps, NextPage } from 'next';

const Hello: NextPage<ServerSideProps> = ({ message }) => {
  return <p>{message}</p>;
}

export default Hello;

type ServerSideProps = {
  message: string;
}

export const getServerSideProps: GetServerSideProps<ServerSideProps> = async () => {
  return {
    props: {
      message: "Hello World",
    },
  };
};

解決案

なんかうまいことコードの一部を隠す方法がないかとNext.jsのDiscussionsで聞いてみましたが、案の定「無いのでは」との回答をもらいました。そこで、別の解決策を考える必要があります。

案1. getServerSidePropsのコードを pages/ 配下のファイルに記述しない

SourceMapに pages/**/*.tsx の内容が丸ごと含まれてしまうことが問題ですので、getServerSidePropsのコードを pages/ 配下に記述しなければSourceMapに含まれることもありません。
この案の実験結果は後述します。

案2. SourceMapへのアクセス元を制限する

Bugsnagであれば、SourceMap取得時のアクセス元IPを公開してくれています。
/_next/**/*.map へのアクセスにIP制限を設けることで、不特定多数に元コードが公開されてしまうのを防ぐことができます。
当然、アクセス元IPがわからない場合は実現不可です。

案3. SourceMapを必要とする場所に送信する

BugsnagやSentryでは、SourceMapをアップロードする機能を提供しています。
ビルド後にSourceMapをアップロードし、SourceMapファイルを削除してからアプリケーションを起動する案も考えられます。
CI/CD環境がないと、手順ミスで意図せず公開してしまうことが容易に想像できます。

案4. SourceMapを生成しない

トラブルシュートの効率がかなり悪くなりますが、getServerSidePropsのコードが公開されるよりはマシというケースも多いでしょう。
あくまで暫定措置であって、速やかに他案への移行をしたいところです。

解決案1の実験

「getServerSidePropsのコードを pages/ 配下のファイルに記述しない」案を実際に試してみます。

前出の pages/hello.tsx のgetServerSideProps関数を server/props/hello.ts に移動します。

pages/hello.tsx
import type { NextPage } from "next";
import type { Props as ServerSideProps } from "../server/props/hello";
export { getServerSideProps } from "../server/props/hello";

const Hello: NextPage<ServerSideProps> = ({ message }) => {
  return <p>{message}</p>;
};

export default Hello;
server/props/hello.ts
import type { GetServerSideProps } from "next";

export type Props = {
  message: string;
};

export const getServerSideProps: GetServerSideProps<Props> = async () => {
  return {
    props: {
      message: "Hello World",
    },
  };
};

ビルドしてSourceMapの内容を見ると、getServerSidePropsのコードが含まれていないことがわかります。
pages/hello.tsx の中身がそのまま入っているだけですので当然です。

$ yarn build
...(snip)...

$ jq -r '.sourcesContent[]' .next/static/chunks/pages/hello-22e1791de54004bb.js.map

    (window.__NEXT_P = window.__NEXT_P || []).push([
      "/hello",
      function () {
        return require("private-next-pages/hello.tsx");
      }
    ]);
    if(module.hot) {
      module.hot.dispose(function () {
        window.__NEXT_P.push(["/hello"])
      });
    }

import type { NextPage } from "next";
import type { Props as ServerSideProps } from "../server/props/hello";
export { getServerSideProps } from "../server/props/hello";

const Hello: NextPage<ServerSideProps> = ({ message }) => {
  return <p>{message}</p>;
};

export default Hello;

雑感

SourceMapの問題を解決するためだけにファイル分割を強いられたくはないもの、サーバー側のみで動作するコードを明確に分離できるという利点もあります。

ところで、この問題について調べても全く情報を見つけられませんでした。みんな困ってないのかな?気付いてないのかな?と思う一方で、当たり前すぎる何かを見逃してるのではないかという思いもあります。何かご存知の方は教えてください。

Discussion