From e373a58cc1fd8c222ee4bcc756e76571d8aaec03 Mon Sep 17 00:00:00 2001 From: kan_shimizu Date: Mon, 5 Oct 2026 11:17:43 +0900 Subject: [PATCH 1/9] feat: add schema-aware client types and inference --- .eslintrc.js | 34 - .github/workflows/ci.yml | 33 +- .github/workflows/release.yml | 13 +- .oxlintrc.json | 98 + .prettierignore | 3 + DEVELOPMENT.md | 107 + README.md | 334 +- README_en.md | 388 +- examples/README.md | 55 + examples/generated/microcms-types.ts | 252 + examples/tsconfig.json | 15 + examples/usage/errors.ts | 57 + examples/usage/read.ts | 24 + examples/usage/scratch.ts | 7 + examples/usage/shared/client.ts | 8 + examples/usage/shared/queries.ts | 11 + examples/usage/type-extraction.ts | 30 + examples/usage/write.ts | 38 + jest.config.js | 11 +- package-lock.json | 3193 ++- package.json | 31 +- scripts/check-type-performance.mjs | 100 + scripts/check-typescript.mjs | 32 + scripts/finish-declarations.mjs | 8 + src/createClient.ts | 21 +- src/index.ts | 5 + src/lib/fetch.ts | 2 +- src/typedSchema.ts | 924 + .../fixtures/typegen/generated/extensions.ts | 7 + .../fixtures/typegen/generated/tree-usage.ts | 44 + tests/fixtures/typegen/generated/tree.ts | 85 + .../typegen/generated/with-extensions.ts | 262 + tests/fixtures/typegen/lifecycle.json | 653 + .../performance/captured-service/generated.ts | 252 + .../performance/captured-service/usage.ts | 35 + .../performance/cyclic-service/generated.ts | 1128 ++ .../performance/cyclic-service/usage.ts | 113 + .../typegen/performance/scenarios.json | 20 + .../performance/wide-service/generated.ts | 16118 ++++++++++++++++ .../typegen/performance/wide-service/usage.ts | 553 + tests/fixtures/typegen/responses.json | 1467 ++ tests/fixtures/typegen/schemas.json | 500 + tests/fixtures/typegen/writes.json | 97 + tests/generatedRelations.integration.mjs | 14 + tests/generatedSchema.integration.mjs | 127 + tests/helpers/fixture-verification.mjs | 159 + tests/helpers/type-compiler.mjs | 44 + tests/typedSchema.matrix.ts | 578 + tests/typedSchema.typecheck.ts | 1472 ++ tsconfig.declarations.json | 9 + tsconfig.json | 14 +- tsconfig.schema-dist.json | 13 + tsconfig.schema-types.json | 16 + tsconfig.tests.json | 12 + tsup.config.ts | 4 +- 55 files changed, 27340 insertions(+), 2290 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 .oxlintrc.json create mode 100644 .prettierignore create mode 100644 DEVELOPMENT.md create mode 100644 examples/README.md create mode 100644 examples/generated/microcms-types.ts create mode 100644 examples/tsconfig.json create mode 100644 examples/usage/errors.ts create mode 100644 examples/usage/read.ts create mode 100644 examples/usage/scratch.ts create mode 100644 examples/usage/shared/client.ts create mode 100644 examples/usage/shared/queries.ts create mode 100644 examples/usage/type-extraction.ts create mode 100644 examples/usage/write.ts create mode 100644 scripts/check-type-performance.mjs create mode 100644 scripts/check-typescript.mjs create mode 100644 scripts/finish-declarations.mjs create mode 100644 src/typedSchema.ts create mode 100644 tests/fixtures/typegen/generated/extensions.ts create mode 100644 tests/fixtures/typegen/generated/tree-usage.ts create mode 100644 tests/fixtures/typegen/generated/tree.ts create mode 100644 tests/fixtures/typegen/generated/with-extensions.ts create mode 100644 tests/fixtures/typegen/lifecycle.json create mode 100644 tests/fixtures/typegen/performance/captured-service/generated.ts create mode 100644 tests/fixtures/typegen/performance/captured-service/usage.ts create mode 100644 tests/fixtures/typegen/performance/cyclic-service/generated.ts create mode 100644 tests/fixtures/typegen/performance/cyclic-service/usage.ts create mode 100644 tests/fixtures/typegen/performance/scenarios.json create mode 100644 tests/fixtures/typegen/performance/wide-service/generated.ts create mode 100644 tests/fixtures/typegen/performance/wide-service/usage.ts create mode 100644 tests/fixtures/typegen/responses.json create mode 100644 tests/fixtures/typegen/schemas.json create mode 100644 tests/fixtures/typegen/writes.json create mode 100644 tests/generatedRelations.integration.mjs create mode 100644 tests/generatedSchema.integration.mjs create mode 100644 tests/helpers/fixture-verification.mjs create mode 100644 tests/helpers/type-compiler.mjs create mode 100644 tests/typedSchema.matrix.ts create mode 100644 tests/typedSchema.typecheck.ts create mode 100644 tsconfig.declarations.json create mode 100644 tsconfig.schema-dist.json create mode 100644 tsconfig.schema-types.json create mode 100644 tsconfig.tests.json diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 12f6352..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'prettier', - ], - rules: { - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-empty-interface': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-object-literal-type-assertion': 'off', - '@typescript-eslint/no-triple-slash-reference': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/no-var-requires': 'off', - '@typescript-eslint/prefer-interface': 'off', - '@typescript-eslint/prefer-namespace-keyword': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - }, - env: { - browser: true, - node: true, - es6: true, - jest: true, - }, - overrides: [ - { - files: ['tests/**'], - plugins: ['jest'], - }, - ], -}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8e25e5..3963d40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,9 +5,9 @@ name: Node.js CI on: push: - branches: ["main"] + branches: ['main'] pull_request: - branches: ["main"] + branches: ['main'] jobs: static-check: @@ -19,8 +19,8 @@ jobs: persist-credentials: false - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version-file: ".node-version" - cache: "npm" + node-version-file: '.node-version' + cache: 'npm' - run: npm ci - name: Format Check run: npm run format @@ -28,6 +28,8 @@ jobs: run: npm run lint - name: Type Check run: npm run typecheck + - name: Schema Type Check + run: npm run typecheck:schema build-and-test: runs-on: ubuntu-slim @@ -45,9 +47,30 @@ jobs: uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ matrix.node-version }} - cache: "npm" + cache: 'npm' - run: npm ci - name: Build run: npm run build + - name: Published Declaration Type Check + run: npm run typecheck:dist + - name: TypeScript Consumer Regression Matrix + run: npm run test:types - name: Unit Tests run: npm run test + + typescript-compatibility: + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: '.node-version' + cache: 'npm' + - run: npm ci + - run: npm run build + - name: Install minimum supported TypeScript separately + run: npm install --prefix "$RUNNER_TEMP/microcms-ts6" --no-save --package-lock=false typescript@6.0.2 + - name: TypeScript 6 consumer compatibility + run: MICROCMS_TYPESCRIPT_BINARY="$RUNNER_TEMP/microcms-ts6/node_modules/typescript/bin/tsc" npm run test:types diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a34da33..d00bf22 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: release on: push: tags: - - "v*" + - 'v*' permissions: id-token: write # Required for OIDC @@ -18,14 +18,21 @@ jobs: persist-credentials: false - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version-file: ".node-version" - registry-url: "https://registry.npmjs.org" + node-version-file: '.node-version' + registry-url: 'https://registry.npmjs.org' package-manager-cache: false - name: release on npm run: | npm ci npm run lint npm run typecheck + npm run typecheck:schema npm run test npm run build + npm run typecheck:dist + npm run test:types + - name: Install minimum supported TypeScript separately + run: npm install --prefix "$RUNNER_TEMP/microcms-ts6" --no-save --package-lock=false typescript@6.0.2 + - name: TypeScript 6 consumer compatibility + run: MICROCMS_TYPESCRIPT_BINARY="$RUNNER_TEMP/microcms-ts6/node_modules/typescript/bin/tsc" npm run test:types - run: npm publish diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..a848202 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,98 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript"], + "categories": { + "correctness": "off" + }, + "env": { + "browser": true, + "node": true, + "es6": true, + "jest": true + }, + "ignorePatterns": [ + "examples/generated/", + "tests/fixtures/typegen/generated/", + "tests/fixtures/typegen/performance/" + ], + "rules": { + "eslint/for-direction": "error", + "eslint/no-array-constructor": "error", + "eslint/no-async-promise-executor": "error", + "eslint/no-case-declarations": "error", + "eslint/no-class-assign": "error", + "eslint/no-compare-neg-zero": "error", + "eslint/no-cond-assign": "error", + "eslint/no-constant-condition": "error", + "eslint/no-control-regex": "error", + "eslint/no-debugger": "error", + "eslint/no-delete-var": "error", + "eslint/no-dupe-else-if": "error", + "eslint/no-duplicate-case": "error", + "eslint/no-empty": "error", + "eslint/no-empty-character-class": "error", + "eslint/no-empty-pattern": "error", + "eslint/no-ex-assign": "error", + "eslint/no-extra-boolean-cast": "error", + "eslint/no-fallthrough": "error", + "eslint/no-global-assign": "error", + "eslint/no-inner-declarations": "error", + "eslint/no-invalid-regexp": "error", + "eslint/no-irregular-whitespace": "error", + "eslint/no-loss-of-precision": "error", + "eslint/no-misleading-character-class": "error", + "eslint/no-nonoctal-decimal-escape": "error", + "eslint/no-prototype-builtins": "error", + "eslint/no-regex-spaces": "error", + "eslint/no-self-assign": "error", + "eslint/no-shadow-restricted-names": "error", + "eslint/no-sparse-arrays": "error", + "eslint/no-unsafe-finally": "error", + "eslint/no-unsafe-optional-chaining": "error", + "eslint/no-unused-labels": "error", + "eslint/no-useless-backreference": "error", + "eslint/no-useless-catch": "error", + "eslint/no-useless-escape": "error", + "eslint/no-var": "error", + "eslint/no-with": "error", + "eslint/prefer-const": "error", + "eslint/prefer-rest-params": "error", + "eslint/prefer-spread": "error", + "eslint/require-yield": "error", + "eslint/use-isnan": "error", + "eslint/valid-typeof": "error", + "typescript/ban-ts-comment": "error", + "typescript/ban-types": "error", + "typescript/no-duplicate-enum-values": "error", + "typescript/no-extra-non-null-assertion": "error", + "typescript/no-misused-new": "error", + "typescript/no-namespace": "error", + "typescript/no-non-null-asserted-optional-chain": "error", + "typescript/no-this-alias": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-unsafe-declaration-merging": "error", + "typescript/prefer-as-const": "error", + "typescript/triple-slash-reference": "error" + }, + "overrides": [ + { + "files": ["**/*.js", "**/*.mjs", "**/*.cjs"], + "rules": { + "eslint/constructor-super": "error", + "eslint/getter-return": "error", + "eslint/no-const-assign": "error", + "eslint/no-dupe-class-members": "error", + "eslint/no-dupe-keys": "error", + "eslint/no-func-assign": "error", + "eslint/no-import-assign": "error", + "eslint/no-obj-calls": "error", + "eslint/no-redeclare": "error", + "eslint/no-setter-return": "error", + "eslint/no-this-before-super": "error", + "eslint/no-undef": "error", + "eslint/no-unreachable": "error", + "eslint/no-unsafe-negation": "error" + } + } + ] +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e90fcee --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +examples/generated/ +tests/fixtures/typegen/generated/ +tests/fixtures/typegen/performance/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..a872394 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,107 @@ +# 開発ガイド + +microcms-js-sdk自体の開発・検証手順です。利用者向けのAPI仕様は[README](README.md)、補完や推論をエディタで試す方法は[利用例](examples/README.md)を参照してください。 + +## 開発環境 + +`.node-version`で指定されたNode.jsを使用し、リポジトリのルートで依存関係をインストールします。 + +```sh +npm ci +``` + +以下のコマンドもリポジトリのルートで実行します。 + +## 開発用コマンド + +| コマンド | 内容 | +| ---------------------------- | ------------------------------------------------------------------- | +| `npm run build` | JavaScriptと公開型定義を`dist/`へ生成 | +| `npm run format` | ソース・テスト・利用例の整形を確認 | +| `npm run lint` | ソース・テスト・利用例のlint | +| `npm run typecheck` | SDK内部の実装と単体テストを型検査 | +| `npm test` | 実行時の単体テスト | +| `npm run test:coverage` | 単体テストとカバレッジ計測 | +| `npm run typecheck:schema` | ソースの公開APIによる型検査 | +| `npm run typecheck:dist` | ビルド済みの公開型定義による型検査 | +| `npm run typecheck:examples` | ビルド済みの公開型定義で利用例を型検査 | +| `npm run test:types` | 開発用TypeScriptで公開API・利用例・回帰テスト・生成型との連携を検査 | +| `npm run test:performance` | 生成済みスキーマを使った型検査の性能測定 | + +`format:fix`・`lint:fix`はファイルを変更して修正するコマンドです。`typecheck:dist`・`typecheck:examples`・`test:types`・`test:performance`はビルド後に実行してください。 + +## テスト + +### 責務 + +| 対象 | 確認すること | +| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `tests/*.test.ts` | 通信、リトライ、クエリ変換、読み取り・書き込みなどの実行時の処理 | +| `tests/typedSchema.typecheck.ts` | 手書きのスキーマを使い、読み取り・書き込みの推論、不正な指定、省略可能なクエリ、従来genericsとの互換性を網羅 | +| `tests/typedSchema.matrix.ts` | 呼び出し方とメソッドを組み合わせ、ソースから抽出した公開型・配布型と`exactOptionalPropertyTypes`の両設定で正常系の型と異常系の検出を検査 | +| `tests/generatedSchema.integration.mjs`・`tests/generatedRelations.integration.mjs` | 生成済みスキーマをSDKに渡し、保存済みレスポンス・書き込み入力・多段と循環参照の推論を確認 | +| `examples/` | 利用者向けの読み書き・型抽出・型エラーの例を公開型定義で検査 | + +型テストはTypeScriptコンパイラによる検査のみです。記述されたクライアント呼び出しを実行したり、実サービスへリクエストを送ったりしません。 + +### 実行方法 + +```sh +npm run format +npm run lint +npm run typecheck +npm test +npm run typecheck:schema +npm run build +npm run typecheck:dist +npm run test:types +``` + +`typecheck:schema`はパッケージのインポート先を`src/index.ts`に、`typecheck:dist`はパッケージのルートに設定し、同じ型テストを実行します。配布型の検査では`package.json`の`types`からビルド済みの`dist/microcms-js-sdk.d.ts`を解決します。ソースの公開APIとビルド後の公開型定義を、それぞれSDK側で確認できます。 + +`typecheck:dist`はビルド後に実行してください。CIとリリース処理でも、ビルド後の必須チェックにしています。 + +### 回帰テストの追加方針 + +不具合を直すときは、再現する異常系と近い正常系をセットで残します。複数メソッドに共通する制約は`tests/typedSchema.matrix.ts`のケースに追加し、呼び出し経路ごとの差をまとめて検査してください。正常系は戻り値の型の一致まで検証します。異常系は型エラーの位置が対象の呼び出し内にあることを確認し、テストの準備コードの誤りで成功しないようにしています。 + +マトリクスの例はコンパイラに渡す文字列です。SDKのコンパイル設定でソースから公開型を抽出し、一時ディレクトリに型定義と利用例を書き出して`tsc`へ渡します。一時ファイルは検査後に削除します。利用者側の`exactOptionalPropertyTypes`はSDK内部の実装のコンパイル設定と区別して検査します。`test:types`はビルド後に実行し、CIとリリース処理の必須チェックにしています。 + +### TypeScriptの対応確認 + +開発用のTypeScriptは`^7.0.2`で7系に揃えています。`npm ci`ではlockfileに記録したバージョンを使用し、通常の`tsc`・型定義の生成・型検査・`npm run test:types`はいずれもこのコンパイラを使用します。`test:types`では静的な型テスト、利用例、223ケース×公開型2種類×`exactOptionalPropertyTypes`2設定、生成型との連携を検査します。 + +利用者向けの対応範囲はTypeScript6.0以降で、開発用コンパイラのバージョンとは別です。CIとリリース処理ではTypeScript6.0.2を別の一時ディレクトリへインストールし、同じ公開型のテストを実行します。ローカルの依存関係には6系を含めません。 + +`test:types`・`test:performance`で使うコンパイラを切り替える場合は、`MICROCMS_TYPESCRIPT_BINARY`にCLIの絶対パスを指定します。この指定は`npx tsc`やエディタが使うTypeScriptのバージョンには影響しません。SDK内部の型定義の生成と`typecheck`は常に開発用の7系を使用します。 + +型テストはCompiler APIに依存せず、CLIで型定義の生成と利用例の検査を行います。lintにはTypeScriptコンパイラに依存しないOxlintを使用し、既存のESLint・TypeScriptの主要なルールに対応する検査を設定しています。TypeScriptの構文検査は`typecheck`・`test:types`で行い、整形にはPrettierを使用します。 + +JavaScriptはtsupで生成し、従来と同じES5ターゲット・CommonJS・ES modules・ブラウザ向け形式を維持します。型定義は`tsconfig.declarations.json`で`dist/types/`へ生成し、`scripts/finish-declarations.mjs`で既存の公開エントリーポイントから再エクスポートします。JestのTypeScript変換には`@swc/jest`を使用し、型検査は`typecheck`・`test:types`で行います。`tsconfig.tests.json`で単体テストも型検査し、テスト用依存の型定義のみ`skipLibCheck`の対象にします。SDK本体と配布型定義の生成では型定義の検査を省略しません。 + +## 生成型との連携とfixture + +`test:types`は、選択したコンパイラで`examples/`と生成型の連携テストも実行します。連携テストは生成済みの型を読み、typegenや隣接リポジトリを必要としません。保存済みJSONを推論された戻り値型へ直接代入し、選択されたフィールドの集合と抽出型の一致も検査します。不正なメタデータ・値・選択肢・未選択フィールドを含むレスポンスが型エラーになることも検査します。ローカルでは7系、CIでは6系でも同じ検査を実行します。 + +[`tests/fixtures/typegen/`](tests/fixtures/typegen/)は確認用の[typegenサービス](https://typegen.microcms.io/)から取得したスキーマ、33ケースの読み取りレスポンス、新規作成入力、作成・更新・削除の実行記録です。`tests/fixtures/typegen/generated/extensions.ts`は手書きの拡張データ型、同じ`generated/`内の他のファイルは拡張データと循環参照を検証する生成型と利用例です。通常のテストは実サービスへアクセスせず、取得・書き込みを実行しません。公開用に、保存済みデータのURLとメールアドレスは`example.com`のダミーへ置き換えています。 + +`all_fields`の`typegen-fixture-filled`・`typegen-fixture-empty`で値ありと空値を確認しました。繰り返しは5種類すべてを含み、数値の0、真偽値のfalse、null、空配列も保存しています。更新・削除は別の専用IDで実行し、検証後に削除済みです。オブジェクト形式の更新は型検査のみです。これらは取得時の記録で、現在のAPI動作を保証するライブテストではありません。 + +型生成の正しさと、スキーマから生成結果への一致はtypegen側で検査します。typegenの出力形式を変更した場合は、このリポジトリの生成済み型を更新し、`npm run test:types`を実行してください。データを更新する場合も入力とレスポンスの対応・取得元・取得日時を維持し、URLとメールアドレスをダミー化してください。認証情報は保存しないでください。 + +## 型チェックの性能確認 + +```sh +npm run build +npm run test:performance +``` + +保存済みの4API、50API・各100フィールド、多段・循環参照の10APIの生成済み型で、`depth: 0`〜`3`、`fields`、型抽出、書き込みを測定します。選択したTypeScriptコンパイラで3回実行し、`tsc --extendedDiagnostics`の結果をJSONで出力します。生成済み型は`tests/fixtures/typegen/performance/`に保存し、typegenの実行やAPI通信は不要です。メモリはコンパイラの報告値で、プロセス全体の最大使用量ではありません。CIの固定時間による合否判定には使いません。 + +## CIとリリース処理 + +[テストCI](.github/workflows/ci.yml)は`main`へのpushとpull requestで実行します。静的検査では`.node-version`のNode.jsを使用し、ビルドとテストはNode.js18・20・22・24系で実行します。`test:types`を通して、開発用のTypeScript7で利用例と生成型との連携も検査します。別ジョブでTypeScript6.0.2による同じ互換性検査を実行します。 + +[リリース処理](.github/workflows/release.yml)は`v`で始まるタグへのpushで実行します。lint・実装の型検査・単体テスト・ビルド・公開型定義と利用者側の型検査が成功した後に、npmへ公開します。リリースタグはレビューと公開の準備が完了してから作成してください。 + +通常の開発用コマンドとテストCIはnpmへの公開を行いません。 diff --git a/README.md b/README.md index c428f0c..549c0ed 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,16 @@ JavaScriptやNode.jsのアプリケーションからmicroCMSのAPIと簡単に 詳細は[SDKの保守方針](https://document.microcms.io/manual/limitations#h8e929adf81)をご覧ください。 +## 動作環境 + +- Node.js 18+(22+を推奨、ブラウザから利用する場合は不要) +- TypeScript6.0+(型を利用する場合) + ## チュートリアル -公式ドキュメントの [チュートリアル](https://document.microcms.io/tutorial/javascript/javascript-top)をご覧ください。 +まず簡単な組み込みを試したい場合は、公式の[JavaScriptチュートリアル](https://document.microcms.io/tutorial/javascript/javascript-top)を参照してください。ブラウザとNode.jsでの基本的な利用手順を確認できます。 -## はじめに +## セットアップ ### インストール @@ -30,9 +35,6 @@ $ npm install microcms-js-sdk $ yarn add microcms-js-sdk ``` -> [!IMPORTANT] -> v3.0.0以上を使用する場合は、Node.jsのv18以上が必要です。 - #### ブラウザ(セルフホスティング) [リリースページ](https://github.com/microcmsio/microcms-js-sdk/releases)から`microcms-js-sdk-x.y.z.tgz`をダウンロードして解凍してください。その後、お好みのサーバーにアップロードして使用してください。対象ファイルは `./dist/umd/microcms-js-sdk.js` です。 @@ -93,24 +95,25 @@ const client = createClient({ }); ``` +TypeScriptでの型指定と推論は[TypeScript](#typescript)を参照してください。 + ### APIメソッド 以下の表は、microCMS JavaScript SDKの各メソッドがリスト形式のAPIまたはオブジェクト形式のAPI、どちらで使用できるかを示しています。 -| メソッド | リスト形式 | オブジェクト形式 | -|-------------------|-------------|---------------| -| getList | ✔️ | | -| getListDetail | ✔️ | | -| getObject | | ✔️ | -| getAllContentIds | ✔️ | | -| getAllContents | ✔️ | | -| create | ✔️ | | -| update | ✔️ | ✔️ | -| delete | ✔️ | | +| メソッド | リスト形式 | オブジェクト形式 | +| ---------------- | ---------- | ---------------- | +| getList | ✅ | | +| getListDetail | ✅ | | +| getObject | | ✅ | +| getAllContentIds | ✅ | | +| getAllContents | ✅ | | +| create | ✅ | | +| update | ✅ | ✅ | +| delete | ✅ | | > [!NOTE] -> - 「リスト形式」の✔️は、APIの型がリスト形式に設定されている場合に使用できるメソッドを示します。 -> - 「オブジェクト形式」の✔️は、APIの型がオブジェクト形式に設定されている場合に使用できるメソッドを示します。 +> 汎用の取得メソッド`get`は非推奨です。`getList`・`getListDetail`・`getObject`を使用してください。スキーマを指定したクライアントでも、`get`の戻り値型は自動推論されません。 ### コンテンツ一覧の取得 @@ -143,7 +146,7 @@ client ids: 'foo', filters: 'publishedAt[greater_than]2021-01-01T03:00:00.000Z', depth: 1, - } + }, }) .then((res) => console.log(res)) .catch((err) => console.error(err)); @@ -176,11 +179,10 @@ client draftKey: 'abcd', fields: 'id,title', depth: 1, - } + }, }) .then((res) => console.log(res)) .catch((err) => console.error(err)); - ``` ### オブジェクト形式のコンテンツの取得 @@ -239,7 +241,7 @@ client #### コンテンツID以外のフィールドの値を全件取得 -`alternateField`プロパティにフィールドIDを指定することで、コンテンツID以外のフィールドの値を全件取得できます。 +`alternateField`プロパティにフィールドIDを指定することで、コンテンツID以外のフィールドの値を全件取得できます。取得した値が文字列でない場合は、実行時にエラーになります。 ```javascript client @@ -272,7 +274,10 @@ client client .getAllContents({ endpoint: 'endpoint', - queries: { filters: 'createdAt[greater_than]2021-01-01T03:00:00.000Z', orders: '-createdAt' }, + queries: { + filters: 'createdAt[greater_than]2021-01-01T03:00:00.000Z', + orders: '-createdAt', + }, }) .then((res) => console.log(res)) .catch((err) => console.error(err)); @@ -453,147 +458,189 @@ client .catch((err) => console.error(err)); ``` -### TypeScript +### CustomRequestInit -`getList`メソッド、`getListDetail`メソッド、`getObject`メソッドはデフォルトのレスポンスの型を定義しています。 +#### Next.js App Router -#### getListメソッドのレスポンスの型 +Next.jsのApp Routerで利用されるfetchのcacheオプションを指定できます。 -```typescript -type Content = { - text: string, -}; -/** - * { - * contents: Content[]; // 設定したスキーマの型を格納する配列 - * totalCount: number; - * limit: number; - * offset: number; - * } - */ -client.getList({ /* その他のプロパティ */ }) -``` +指定可能なオプションは、Next.jsの公式ドキュメントを参照してください。 -#### getListDetailメソッドのレスポンスの型 +[Functions: fetch \| Next\.js](https://nextjs.org/docs/app/api-reference/functions/fetch) -```typescript -type Content = { - text: string, -}; -/** - * { - * id: string; - * createdAt: string; - * updatedAt: string; - * publishedAt?: string; - * revisedAt?: string; - * text: string; // 設定したスキーマの型 - * } - */ -client.getListDetail({ /* その他のプロパティ */ }) +```ts +const response = await client.getList({ + customRequestInit: { + next: { + revalidate: 60, + }, + }, + endpoint: 'endpoint', +}); ``` -#### getObjectメソッドのレスポンスの型 +#### AbortController: abortメソッド -```typescript -type Content = { - text: string, -}; -/** - * { - * createdAt: string; - * updatedAt: string; - * publishedAt?: string; - * revisedAt?: string; - * text: string; // 設定したスキーマの型 - * } - */ -client.getObject({ /* その他のプロパティ */ }) -``` +fetchリクエストを中断できます。 -#### getAllContentIdsメソッドのレスポンスの型 +```ts +const controller = new AbortController(); +const timeoutId = setTimeout(() => { + controller.abort(); +}, 1000); -```typescript -/** - * string[] - */ -client.getAllContentIds({ /* その他のプロパティ */ }) +try { + const response = await client.getObject({ + customRequestInit: { + signal: controller.signal, + }, + endpoint: 'config', + }); +} finally { + clearTimeout(timeoutId); +} ``` -#### 型安全なコンテンツの登録 +### TypeScript -`content`の型は`Content`であるため、型安全なコンテンツの登録が可能です。 +補完や推論をエディタで試す場合は、[型付きクライアントの利用例](examples/README.md)を参照してください。 -```typescript -type Content = { - title: string; - body?: string; -}; +コンテンツAPIの型は、サービス全体のスキーマをクライアント生成時に指定する方法と、メソッドごとに型を指定する方法があります。取得クエリに対応したコンテンツ型だけを取り出すこともできます。 -client.create({ - endpoint: 'endpoint', - content: { - title: 'タイトル', - body: '本文', - }, +#### クライアント生成時にスキーマを指定する + +[microcms-typegen](https://github.com/microcmsio/microcms-ts-typegen#readme)で作成したサービスのスキーマ型を渡すと、TypeScriptのコンパイル時に次の型検査・推論が利用できます。 + +- **入力の型検査**:登録済みのエンドポイントとAPI形式、静的に確定する`fields`のフィールドパスと`depth`の組み合わせを検査します。クエリの項目名、値の基本型、`depth`の範囲(0〜3)も検査します。 +- **戻り値型の推論**:エンドポイントと`depth`・`fields`に応じて、`getList`、`getListDetail`、`getObject`、`getAllContents`の戻り値型を推論します。 + +以下は型生成器の設定が`services.main`の場合の例です。スキーマ型の名前はサービス名に応じて変わります。 + +```ts +import { createClient } from 'microcms-js-sdk'; +import type { MainServiceSchema } from './microcms-types'; + +const client = createClient({ + serviceDomain: 'YOUR_DOMAIN', + apiKey: 'YOUR_API_KEY', +}); + +const response = await client.getList({ + endpoint: 'blogs', + queries: { depth: 2, fields: ['title', 'category.name'] }, }); ``` -#### 型安全なコンテンツの編集 +##### 型検査・推論の対象外 -`content`は`Partial`型であるため、編集したいプロパティだけを渡せます。 +以下の項目は型検査・推論には未対応です。今後対応を検討します。 -```typescript -type Content = { - title: string; - body?: string; -}; +| 項目 | 検査・推論しない内容 | +| --- | --- | +| `filters` | フィールド名・演算子・値の正しさ | +| `orders` | フィールド名・並び順指定の正しさ | +| `alternateField` | 指定先フィールドの存在と値の型 | +| `richEditorFormat: 'object'` | 戻り値型の自動推論。利用時はメソッドの型引数が必要(省略すると型エラー) | -client.update({ - endpoint: 'endpoint', - content: { - body: '本文', - }, +#### メソッドごとに型を指定する + +従来どおり、スキーマ型を渡さずにクライアントを作成し、読み取りメソッドの型引数を指定できます。型付きクライアントでも明示的なメソッド型引数を使用できます。この呼び出しではスキーマによる`depth`・`fields`の型推論より指定した型を優先します。指定した型と実際のレスポンスが一致するかは利用側で確認してください。 + +```ts +import { createClient } from 'microcms-js-sdk'; + +type Content = { title: string }; +const client = createClient({ + serviceDomain: 'YOUR_DOMAIN', + apiKey: 'YOUR_API_KEY', +}); + +const list = await client.getList({ endpoint: 'blogs' }); +const detail = await client.getListDetail({ + endpoint: 'blogs', + contentId: 'blog-id', }); +const object = await client.getObject({ endpoint: 'settings' }); ``` -### CustomRequestInit +#### クエリに応じたコンテンツ型を取り出す -#### Next.js App Router +コンポーネントのpropsなどで、取得時と同じクエリに対応したコンテンツ1件の型が必要な場合は`InferMicroCMSContent`を使います。 -Next.jsのApp Routerで利用されるfetchのcacheオプションを指定できます。 +```ts +import { createClient, type InferMicroCMSContent } from 'microcms-js-sdk'; +import type { MainServiceSchema } from './microcms-types'; -指定可能なオプションは、Next.jsの公式ドキュメントを参照してください。 +const client = createClient({ + serviceDomain: 'YOUR_DOMAIN', + apiKey: 'YOUR_API_KEY', +}); +const queries = { depth: 2, fields: ['title', 'category.name'] } as const; +const response = await client.getList({ endpoint: 'blogs', queries }); -[Functions: fetch \| Next\.js](https://nextjs.org/docs/app/api-reference/functions/fetch) +type BlogCardProps = { + blog: InferMicroCMSContent; +}; +``` + +このようにクエリを変数に分ける場合は、`as const`で`depth`と`fields`の具体的な値を保持します。`const queries = { ... }`だけでは`depth`は`number`、`fields`は`string[]`に広がります。メソッドへクエリを直接書く場合は`as const`は不要です。 + +`InferMicroCMSContent`だけで不正な`fields`の型を宣言しても、その場では型エラーにならず、結果は`unknown`です。同じクエリを型付きクライアントへ渡すと、静的に分かる誤記は呼び出し箇所で型エラーになります。 + +#### スキーマから書き込みデータの型を推論する + +型生成器で作成した`MainServiceSchema`を指定すると、`create`・`update`の`endpoint`からフィールド名と値の型を推論します。参照はコンテンツID、画像とファイルはURL文字列を指定します。`create`・`delete`はリスト形式API、`update`はリスト形式・オブジェクト形式APIで利用できます。`delete`もエンドポイントを型検査します。 ```ts -const response = await client.getList({ - customRequestInit: { - next: { - revalidate: 60, - }, - }, - endpoint: 'endpoint', +import { createClient } from 'microcms-js-sdk'; +import type { MainServiceSchema } from './microcms-types'; + +const client = createClient({ + serviceDomain: 'YOUR_DOMAIN', + apiKey: 'YOUR_API_KEY', +}); + +await client.create({ + endpoint: 'blogs', + content: { title: 'タイトル', category: 'category-id' }, +}); +await client.update({ + endpoint: 'blogs', + contentId: 'blog-id', + content: { title: '更新したタイトル' }, }); +await client.delete({ endpoint: 'blogs', contentId: 'blog-id' }); ``` -#### AbortController: abortメソッド +生成された入力型は`MainServiceSchema['blogs']['create']`や`MainServiceSchema['blogs']['update']`として取り出せます。 -fetchリクエストを中断できます。 +#### メソッドごとに書き込み型を指定する + +従来の`create`・`update`も利用できます。型付きクライアントで明示すると、その呼び出しでは生成スキーマの書き込み型より指定した型を優先します。`create`は`Content`、`update`は`Partial`として`content`を検査します。指定した型とAPIスキーマの一致は利用側で確認してください。 ```ts -const controller = new AbortController(); -const response = await client.getObject({ - customRequestInit: { - signal: controller.signal, - }, - endpoint: 'config', +type TitleOnly = { title: string }; + +await client.update({ + endpoint: 'blogs', + contentId: 'blog-id', + content: { title: '更新したタイトル' }, }); +``` -setTimeout(() => { - controller.abort(); -}, 1000); +### ヒント + +#### 読み取り用と書き込み用で別々のAPIキーを使用する + +```javascript +const readClient = createClient({ + serviceDomain: 'serviceDomain', + apiKey: 'readApiKey', +}); +const writeClient = createClient({ + serviceDomain: 'serviceDomain', + apiKey: 'writeApiKey', +}); ``` ## マネジメントAPI @@ -696,9 +743,11 @@ client ### TypeScript -#### uploadMediaメソッドのパラメータの型 +#### メディアアップロードの引数 -```typescript +マネジメントAPIの`uploadMedia`は、次の形式のデータを受け取ります。 + +```ts type UploadMediaRequest = | { data: File } | { data: Blob; name: string } @@ -713,9 +762,9 @@ function uploadMedia(params: UploadMediaRequest): Promise<{ url: string }>; ## エラーハンドリング -microCMS APIへのリクエストに失敗した場合、エラーは通常の`Error`として扱えます。`console.error(error)`でエラーメッセージとスタックトレースを出力でき、HTTPエラーの場合はHTTPステータスとAPIから返されたエラーメッセージも含まれます。 +コンテンツAPIとマネジメントAPIのどちらでも、SDKが生成したリクエストエラーは通常の`Error`として扱えます。`console.error(error)`でエラーメッセージとスタックトレースを出力でき、HTTPエラーの場合はHTTPステータスとAPIから返されたエラーメッセージも含まれます。 -さらに、`isMicroCMSRequestError`でエラーを判定すると、リクエストに関する追加情報として`status`、`url`、`originalError`を個別に参照できます。 +SDKが生成したリクエストエラーは、両クライアントで共通の`isMicroCMSRequestError`を使って判定できます。判定後は`status`、`url`、`originalError`を個別に参照できます。以下はコンテンツAPIの例です。マネジメントAPIの`uploadMedia`でも、microCMSへのリクエストでSDKが生成したエラーは同様に判定できます。 ```typescript import { createClient, isMicroCMSRequestError } from 'microcms-js-sdk'; @@ -740,11 +789,13 @@ try { } ``` -| プロパティ | HTTPエラー | ネットワークエラー | -| --------------- | ----------------------------- | --------------------------- | -| `status` | HTTPステータスコード | `undefined` | -| `url` | リクエスト先URL | リクエスト先URL | -| `originalError` | `undefined` | `fetch`が投げた元の値 | +| プロパティ | HTTPエラー | ネットワークエラー | +| --------------- | -------------------- | --------------------- | +| `status` | HTTPステータスコード | `undefined` | +| `url` | リクエスト先URL | リクエスト先URL | +| `originalError` | `undefined` | `fetch`が投げた元の値 | + +`uploadMedia`の引数検証やアップロード元URLの取得で起きたエラーは、microCMSへのリクエスト前に発生するため、`isMicroCMSRequestError`で判定できない場合があります。 `url`に`draftKey`が含まれる場合、その値は`***`にマスクされます。リクエストヘッダー、リクエストボディ、`Response`オブジェクトはエラーへ追加されません。 @@ -752,20 +803,9 @@ try { 追加されるプロパティは非列挙です。そのため、既存の`message`、`toString()`、`Object.keys()`、`JSON.stringify()`の結果には影響しません。 -## ヒント - -### 読み取り用と書き込み用で別々のAPIキーを使用する +## 開発者向け情報 -```javascript -const readClient = createClient({ - serviceDomain: 'serviceDomain', - apiKey: 'readApiKey', -}); -const writeClient = createClient({ - serviceDomain: 'serviceDomain', - apiKey: 'writeApiKey', -}); -``` +SDKの開発環境・テスト・fixture・CIについては、[開発ガイド](DEVELOPMENT.md)を参照してください。 ## ライセンス diff --git a/README_en.md b/README_en.md index 1cd0488..7bcd78b 100644 --- a/README_en.md +++ b/README_en.md @@ -12,11 +12,16 @@ The current maintenance level of this SDK is `Active`. For details, see the [SDK maintenance policy](https://document.microcms.io/en/manual/limitations#h36932e94f2). +## Requirements + +- Node.js 18+ (22+ recommended; not required when using the SDK in a browser) +- TypeScript 6.0+ (when using types) + ## Tutorial -See the [official tutorial](https://document.microcms.io/tutorial/javascript/javascript-top). +To try a basic integration first, follow the official [JavaScript tutorial](https://document.microcms.io/tutorial/javascript/javascript-top). It covers getting started in a browser and with Node.js. -## Getting started +## Setup ### Installation @@ -30,10 +35,7 @@ or $ yarn add microcms-js-sdk ``` -> [!IMPORTANT] -> v3.0.0 or later requires Node.js **v18 or higher**. - -#### Browser(Self-hosting) +#### Browser (self-hosting) Download and unzip `microcms-js-sdk-x.y.z.tgz` from the [releases page](https://github.com/microcmsio/microcms-js-sdk/releases). Then, host it on any server of your choice and use it. The target file is `./dist/umd/microcms-js-sdk.js`. @@ -41,7 +43,7 @@ Download and unzip `microcms-js-sdk-x.y.z.tgz` from the [releases page](https:// ``` -#### Browser(CDN) +#### Browser (CDN) Please load and use the URL provided by an external provider. @@ -74,7 +76,7 @@ or import { createClient } from 'microcms-js-sdk'; //ES6 ``` -#### Usage with a browser +#### Browser ```html