はじめに

みなさん「Yahoo!広告スクリプト」をご存知でしょうか。
Yahoo!広告スクリプトとは、Yahoo! JAPANにおけるWeb広告プラットフォームにアクセスするためのスクリプトです。

Yahoo!広告 スクリプトとは?特徴や利用手順を解説 | LINEヤフー for business

弊社にはWeb広告の運用代行事業があり、とある集計作業でのスクリプト制作を試みました。

Yahoo!広告スクリプトは専用の編集画面でJavaScriptのコードを登録します。
リアルタイム実行またはタイマー実行で、ログを見てトライ&エラーを繰り返すのが基本の使い方です。CI、テスト、リビジョン管理といったなじみの道具はなく、使用できるAPIや構文はECMAScriptのバージョンとは一致しません。

そんなランタイムのための開発環境をどう作るか…🤔
試行錯誤の結果できあがった開発環境について紹介します。

※ Yahoo!広告ランタイムは定期的にアップデートされます。当記事はランタイム「202501」を前提としています。

目指したもの

できるだけ普段に近づけたいと思い、以下を開発環境の要件としました。

  • GitHubでリビジョン管理する事
  • TypeScriptで開発する事
  • ユニットテストを実行する事
  • ランタイムのビルトインサービスを表現できる事
  • ランタイムのシンタックスエラーをローカルで再現する事
  • 1回限りでなく既存プロジェクトへの展開ができる事

完成したもの

https://github.com/ringtail003/yas-scaffold

完成した開発環境をGitHubリポジトリにアップしました。
このリポジトリは以下のように使用します。

空のディレクトリを作成し、npxでセットアップ用スクリプトを叩くと各種設定ファイルがコピーされます。

$ node --version
> v22.9.0

$ mkdir my-project
$ cd my-project

$ npx ringtail003/yas-scaffold yas --init
> 設定ファイルをコピーしました(package.json)
> 設定ファイルをコピーしました(eslint.config.json)
> ...

プロジェクト名を書き換えてパッケージをインストールします。

// package.json
{
  "name": "my-project"
}
$ npm install

これでセットアップは完了です。
サンプルコードに対してユニットテストとLintが実行できます。

$ npm test
> ✔ src/greet.ts (72.979375ms)
>   ✔ プリミティブ (0.4155ms)
>   ✔ 配列 (0.474125ms)
> ...
> ℹ tests 13
> ℹ pass 13
$ npm run lint
> eslint --fix ./src

ビルドコマンドを叩くと、JavaScriptにトランスパイルしたビルドファイルが生成されます。

$ npm run publish
> created dist/bundle.js in 343ms
> created dist/publish.js

ビルドファイルの中身をスクリプト編集画面に貼り付けます。
保存してリアルタイム実行し、ログやリファレンスを見ながら実装を詰めていきます。


リポジトリはYahoo!広告スクリプトのプロジェクトを構成する目的として、yas-scaffold(Yahoo Ads Script Scaffold)と名付けました。以降はこのリポジトリを「YAS開発環境」と表記します。

GitHubでリビジョン管理する

ビルドファイルの冒頭にはgitのコミットが記載されます。
Yahoo!広告スクリプトの編集画面から辿ってビルド時点のコミットが特定できる、という仕組みです。

// publish.js

/**
 * @file ...
 * @see https://github.com/{SAMPLE_ORGANIZATION}/my-project/commit/123abcd
 */
function main () { ... }

コメントはビルド後に publish.sh で埋め込んでいます。{SAMPLE_ORGANIZATION} は実際には自社のOrganizationをベタ書きしていて、 my-project はビルド時にpackage.jsonから読み込みます。

Yahoo!広告スクリプトをたくさん登録しても(上限100個)、どのリポジトリで開発しているのか判別ができます。

TypeScriptで開発する

TypeScriptからJavaScriptへのトランスパイルは Rollup を採用しました。

最初は Babel を使っていたのですが、設定が圧倒的にシンプルであること、複数ファイルを結合するバンドル機能を標準で備えていること、などの優位性からRollupに乗り換えることにしました。

セットアップ用スクリプトで作成されるサンプルコードは index.ts で、開発言語はTypeScriptです。ビルドコマンド を叩くとJavaScriptのファイルが出力されます。

$ npm run build

├── dist
│   ├── bundle.js # ビルドファイル
├── src
│   ├── index.ts # ソースコード
│   └── greet.ts # ソースコード
├── rollup.config.js

設定ファイル rollup.config.js はたったの14行です。巷ではゼロコンフィグのトランスパイラも登場しているようですが、今のところRollupに満足しています。

ユニットテストを実行する

テストはNode.jsの ビルトインテストランナー を採用しました。YAS開発環境では関数のIN/OUTを検証するテストが主体で、必要最低限のものが揃えば十分だと思ったからです。

テストコマンド は基本的に node --test を実行しているだけです。ビルトインテストランナーは *.test.ts を探してテストスイートを実行します。

node --test \
  --experimental-strip-types \ # 型宣言を削除する
  --experimental-transform-types \ # enumなどTypeScript独自のシンタックスを変換する
  --no-warnings=ExperimentalWarning # experimentalの警告表示をオフにする

テストのサンプルコード index.test.ts では、インポート文に .ts が付いています。Node.jsの --experimental-strip-types フラグは型宣言を削除するだけでTypeScriptのように拡張子を補完しないため、拡張子付きで宣言する必要があります。また型のインポートが削除対象となるように type キーワードも必要になります。

import { greet, type Greeting } from "./greet.ts";

インポートだけ気をつければ後は普通にテストを書くことができます。一般的なアサーションも揃っています。

import { test, type TestContext } from "node:test";

describe("...", () => {
  test("...", (t: TestContext) => {
    t.assert.equal("Hello World", "Hello " + "World");
    t.assert.deepEqual([1,2], [1,2]);

    const a = {};
    const b = a;
    t.assert.strictEqual(a, b);
  });
});

テスト環境のためのパッケージのインストールやアップデートメンテも不要ですし、ユニットテストならこれで十分かなと思っています。

--experimental-strip-types フラグの挙動は Node.js v23.6 でデフォルトになるため、Node.jsのアップデートとともに必要がなくなります。

ランタイムのビルトインサービス

リファレンス | Yahoo! JAPAN広告

Yahoo!広告スクリプトにはビルトインサービスが存在し、Googleスプレッドシートの書き込みやログ出力ができます。

const ss = SpreadsheetApp.openById("テスト");
Logger.log("完了");

SpreadsheetAppLogger はYahoo!広告のランタイムで実体が得られるオブジェクトで、そのまま書くとTypeScriptの世界ではコンパイルエラーが発生します。

const ss = SpreadsheetApp.openById("テスト");
//         ~~~~~~~~~~~~~~
//         ERROR: Cannot find name 'SpreadsheetApp'.ts(2304)

YAS開発環境では、ビルトインサービスをグローバルオブジェクトとして builtin.d.ts で型宣言しています。

declare global {
  type SpreadsheetApp = {
    openById (id: string): Spreadsheet;
  };

  // グローバルオブジェクトの存在を宣言する
  var SpreadsheetApp: SpreadsheetApp;

tsconfig.json でデフォルトで読み込むようにしておけば、どのファイルからもインポート文なしで使用できます。

"compilerOptions": {
  "typeRoots": ["./types"]
}
SpreadsheetApp.openById(spreadsheetId); // 👍

テスト環境では実体が得られないため、モックをセットしておきましょう。

beforeEach(() => {
  globalThis.SpreadsheetApp = {
    openById: () => "dummy file",
  };
});

test("...", () => {
  SpreadsheetApp.openById(spreadsheetId); // 👍
});

シンタックスエラーをローカルで再現する

Yahoo!広告のランタイムとECMAScriptのバージョンは一致しません。Array.flatMap(ES2019) は使用できて、それよりも古い class(ES6) は使用できない、といった具合です。

このような環境の場合、以下のような選択肢が考えられます。

  • 新しいバージョンをベースにしてシンタックスを個別に禁止する
  • 古いバージョンをベースにしてシンタックスを個別に許容する

YAS開発環境では「古いバージョン」をベースにしました。新しいシンタックスをひとつずつ試して禁止していくよりも、ランタイムで大半のシンタックスが使える古いバージョンをベースにするほうが楽だからです。

tsconfig.json には、やや古めの ES6 を指定しています。

"compilerOptions": {
  "lib": ["ES6"], // ES6 === ES2015

シンタックスを禁止する(ESLint)

class(ES6) はTypeScriptではコンパイルエラーになりませんが、Yahoo!広告スクリプトのランタイムでシンタックスエラーが発生します。

class Foo {} // コンパイルエラーにならない

このようなケースでは、うっかりコードを書かないように eslint.config.js のルールで使用を禁止します。

import esPlugin from "eslint-plugin-es";

export default [
  {
    files: ["**/*.ts"],
    plugins: {
      es: esPlugin, // `es/` プレフィクスを有効にする
      ...
    },
    rules: {
      "es/no-classes": "error", // classの使用を禁止する
    },
  }
];
class Foo {}
// ~~~~~~~~~
// ERROR: ES2015 class declarations are forbidden.

シンタックスを許容する(declare宣言)

開発中に Object.groupBy(ES2024) が使いたくなることがありました。YAS開発環境はES6のため、ES2024のAPIの型は存在しません。そのまま書くと型の解決ができずコンパイルエラーが発生します。

Object.groupBy([], function () {});
//     ~~~~~~~~
//     ERROR: Property 'groupBy' does not exist on type 'ObjectConstructor'.

Object.groupBy はYahoo!広告スクリプトのランタイムではシンタックスエラーにならず、期待通りの結果が返ることが分かりました。このようなケースでは、declare宣言で型を解決します。

declare global {
  interface ObjectConstructor {
    groupBy<K, T>(...): Partial<Record<K, T[]>>;
  }
}

Object.groupBy([], function () {}); // 👍

シンタックスを許容する(Rollup)

YAS開発環境はES6のため Optional chaining(ES2020) を使うとコンパイルエラーが発生します。この構文はYahoo!広告ランタイムでもシンタックスエラーが発生し、使うことができません。

const v = foo?.bar?.value;
// ~~  ~~
// ERROR: ES2020 optional chaining is forbidden.

このようなケースでは、Rollupでポリフィルできないか探しましょう。

Rollupでは generatedCode で指定したECMAScriptのバージョンにより、一部のコードがポリフィルの対象になります。

export default {
  output: {
    generatedCode: "es5", // ES5で出力する
    ...
  },
// Before: index.ts
const v = foo?.bar?.value

// After: publish.js
(_v = foo === null || foo === void 0 ? void 0 : foo.bar) === null
  || _v === void 0 ? void 0 : _v.value;

ポリフィルできることが分かれば eslint.config.js で許容しておきましょう。

export default [
  {
    ...
    rules: {
      "es/no-optional-chaining": "off", // ルールをオフにする
    },
  }
];
const v = foo?.bar?.value;  // 👍

シンタックスを禁止する(ESLintカスタムルール)

Yahoo!広告ランタイムでは という文字が使用できませんでした。コメントアウトの中に存在していても「使用できない文字が含まれている」と保存時にエラーになる事がありました。

Yahoo!広告ランタイムの独自エラーのため、TypeScriptのコンパイルエラーでは検出できません。

Logger.log("1〜100件のデータ取得完了");  // コンパイルエラーにならない

このようなケースでは Custom Rule Tutorial を参考にしてESLintのカスタムルールを追加します。YAS開発環境では plugin.js にカスタムルール置き場を用意しています。

const plugin = {
  meta: {
    name: "eslint-plugin-no-wave-dash", // ルール名
    version: "1.0.0",
  },
  rules: {
    "no-wave-dash": { // ルールの実装
      create(context) {
        return {
          Literal(node) {
            if (node.value && typeof node.value === "string") {
              if (node.value.includes("")) {
                context.report({
                  node,
                  message: "Yahoo!広告スクリプトで'〜'は使えません。",
                });
              }
              ...

追加したカスタムルールは eslint.config.js で有効にします。

import plugin from "./lint/plugin.js";

export default [
  {
    plugins: {
      custom: plugin,  // `custom/` プレフィクスを有効にする
      ...
    },
    rules: {
      "custom/no-wave-dash": "error", // 禁止する
      ...
    },

このようにすると、Yahoo!広告スクリプトの独自エラーを検出できるようになります。

Logger.log("1〜100件のデータ取得完了");
//          ~~~~~~~~~~~~~~~~~~~~~
// ERROR: Yahoo!広告スクリプトで'〜'は使えません。eslint(custom/no-wave-dash)

既存プロジェクトへの展開

セットアップ用スクリプトを使うことで、同じYAS開発環境のプロジェクトをいくつも作ることができます。

$ npx ringtail003/yas-scaffold yas --init

Yahoo!広告ランタイムのアップデートでそれまで使えなかったシンタックスが使えるようになったり、Node.jsのアップデートで便利な機能が使えるようになるかもしれません。YAS開発環境もそれに合わせて進化していくでしょう。

そんな時のために更新用のスクリプトも用意しています。設定ファイルを再びリポジトリからコピーして、パッケージ更新を促すだけのスクリプトです。

$ npx quartetcom/yas-scaffold yas --update
> 設定ファイルをコピーしました(eslint.config.js)
> ...
> 
> package.json 'devDependencies' をインストールしてください
> ┌─────────┬─────────────┬─────────┬──────────┐
>(index) │ name        │ current │ required │
> ├─────────┼─────────────┼─────────┼──────────┤
> │ 0       │ 'package-A''1.0.0''3.0.0'> └─────────┴─────────────┴─────────┴──────────┘
> npm install -D \
>   package-A@3.0.0

実装は yas-cli.js に存在します。package.json に登録することでnpxでの実行が可能になります。

"bin": {
  "yas": "./bin/yas-cli.js"
}

セットアップ用スクリプトで作成したプロジェクトでは、テストランナーの変更など個別のカスタマイズを想定していません。ベースとなるYAS開発環境のリポジトリに変更を加え更新用スクリプトで配布します。

このような仕組みにすることで、プロジェクトの数が増えてもYahoo!広告ランタイムやNode.jsのアップデートにかんたんに追随できると考えました。

おわりに

Yahoo!広告スクリプトに限らず、他のランタイムでも同じ仕組みで開発環境が構築できるのではないかと思います。

今回紹介した内容がどなたかのお役に立てば幸いです。