Closed80

パッケージマネージャを作ってみたい

shingo.sasakishingo.sasaki

README 要約

  • 本パッケージは超超シンプルなパッケージマネージャー説明用のデモパッケージ
  • 本パッケージの目的はパッケージマネージャーの再開発ではなく、パッケージマネージャーの挙動をコードとコメントを通じて説明すること
  • シンプルさを重視しているので、各エッジケースやエラー対応が欠けてるので、興味がある人は npm や yarn のコードを読んでみよう
shingo.sasakishingo.sasaki

本パッケージは以下のミニマム機能がある

  • パッケージを node_modules ディレクトリにダウンロードする
  • シンプルな CLI を提供する
  • シンプルな依存関係の解決
  • フラットな依存ツリーの生成
  • pacakge-lock.json / yarn.lock ライクな lock ファイルの生成

そこそこ楽しめそうなボリュームではある。

shingo.sasakishingo.sasaki

本パッケージにはステップバイステップで理解するコンテンツにあたるものはないので、実際に本パッケージを使ってみつつ、挙動を確認してからソースコードの全容を見て、そこから少しずつ写経していくってのが良さそう。

shingo.sasakishingo.sasaki

セットアップ

グローバルインストールしてバイナリが入ってること確認。

$ npm install -g tiny-package-manager
$ which tiny-pm
/usr/local/bin/tiny-pm
shingo.sasakishingo.sasaki

適当にディレクトリを作成し、 package.json をミニマムに作成する

package.json
{
  "name": "tiny-pm-test",
  "version": "1.0.0"
}

実行してみる。

$ tiny-pm
[1/2] Finished resolving.

lock ファイルっぽいものが作成される。まだ空だけど。

tiny-pm.yml
{}
shingo.sasakishingo.sasaki

見た感じはパッケージを追加するコマンドが CLI に用意されてない。

$ tiny-pm --help
tiny-pm <command> [args]

コマンド:
  tiny-pm install  Install the dependencies.
  tiny-pm          Install the dependencies.                        [デフォルト]

オプション:
  --production   Install production dependencies only.                    [真偽]
  -v, --version  バージョンを表示                                         [真偽]
  -h, --help     ヘルプを表示                                             [真偽]

ので、package.json に直接依存関係を追記しちゃう

package.json
{
  "name": "tiny-pm-test",
  "version": "1.0.0",
  "dependencies": {
    "axios": "^0.27.2"
  }
}

それでインストールを実行する。

$ tiny-pm
[1/2] Finished resolving.
[2/2] Installing [########]

lock ファイルが更新されて、おそらく axios に必要なファイルが揃ってる。

tiny-pm.yml
asynckit@^0.4.0:
  version: 0.4.0
  url: 'https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz'
  shasum: c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79
  dependencies: {}
axios@^0.27.2:
  version: 0.27.2
  url: 'https://registry.npmjs.org/axios/-/axios-0.27.2.tgz'
  shasum: 207658cc8621606e586c85db4b41a750e756d972
  dependencies:
    follow-redirects: ^1.14.9
    form-data: ^4.0.0
combined-stream@^1.0.8:
  version: 1.0.8
  url: 'https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz'
  shasum: c3d45a8b34fd730631a110a8a2520682b31d5a7f
  dependencies:
    delayed-stream: ~1.0.0
delayed-stream@~1.0.0:
  version: 1.0.0
  url: 'https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz'
  shasum: df3ae199acadfb7d440aaae0b29e2272b24ec619
  dependencies: {}
follow-redirects@^1.14.9:
  version: 1.15.1
  url: 'https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz'
  shasum: 0ca6a452306c9b276e4d3127483e29575e207ad5
  dependencies: null
form-data@^4.0.0:
  version: 4.0.0
  url: 'https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz'
  shasum: 93919daeaf361ee529584b9b31664dc12c9fa452
  dependencies:
    asynckit: ^0.4.0
    combined-stream: ^1.0.8
    mime-types: ^2.1.12
mime-db@1.52.0:
  version: 1.52.0
  url: 'https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz'
  shasum: bbabcdc02859f4987301c856e3387ce5ec43bf70
  dependencies: null
mime-types@^2.1.12:
  version: 2.1.35
  url: 'https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz'
  shasum: 381a871b62a734450660ae3deee44813f70d959a
  dependencies:
    mime-db: 1.52.0

さらに node_modules ディレクトリに依存パッケージが含まれている。

$ ls node_modules/
asynckit         axios            combined-stream  delayed-stream   follow-redirects form-data        mime-db          mime-types

試しに axios を使ったコードを実行してみる。

main.js
const axios = require('axios').default
axios.get('https://jsonplaceholder.typicode.com/todos/').then(res => {
  console.log(res.data.length)
})
$ node main.js 
200

動いてそう。

shingo.sasakishingo.sasaki

pacakge.json から axios を削除して再度 tiny-pm を実行しても削除はされない。
本パッケージでは追加しかできないみたい。

shingo.sasakishingo.sasaki

index.ts

  • CLI からパラメータを受け取って各種ロジックを呼び出すコントローラ的な関数
  • ここはコメントが丁寧に書かれてるので読みながら理解深められそう

https://github.com/g-plane/tiny-package-manager/blob/master/src/index.ts

処理の流れはこんな感じ。

  • package.json を親ディレクトリに向かって探索し、あれば読み込む
  • インストールするパッケージ一覧を、 package.json 及び install コマンドの引数から取り出す
    • 後者については実装されてるように見えるけど動作しない…。 (tiny-pm install axios みたいなの)
  • --production オプションが付与されている場合は、インストールリストから devDependencies を削除する
  • 現状の lock ファイルを読み込んでおく (lock.ts)
  • インストールするパッケージ一覧の情報を取得する (list.ts)
  • 変更差分を lock ファイルに書き込む (lock.ts)
  • インストールするパッケージに関するログを書き出す (log.ts)
  • パッケージを npm から インストール (install.ts)

実際の依存解決をどうやってるのかなぁと思ったけど、npm レジストリからメタデータが取れるのね
https://registry.npmjs.org/axios

shingo.sasakishingo.sasaki

全体的な流れはわかったけど、モジュールを分割してる割にモジュールの独立性が低くて、副作用をガンガン起こすしそれに期待するコードがあってあまりクリーンではないように見える (少なくとも好みではない)

4年も前のリポジトリってのもあるし、ここは写経じゃなくてコード読みながら自分なりにゼロベースで作っていく方向が良いかも?

shingo.sasakishingo.sasaki

好みの構成でプロジェクトを作ってみる

$ npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint typescript prettier
package.json
{
  "name": "@s-sasaki-0529/tiny-package-manager",
  "version": "0.0.1",
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^5.35.1",
    "@typescript-eslint/parser": "^5.35.1",
    "eslint": "^8.23.0",
    "prettier": "^2.7.1",
    "typescript": "^4.8.2"
  },
  "eslintConfig": {
    "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
    "parser": "@typescript-eslint/parser",
    "plugins": ["@typescript-eslint"],
    "root": true
  },
  "prettier": {
    "semi": false,
    "singleQuote": true,
    "trailingComma": "none",
    "printWidth": 120,
    "arrowParens": "avoid"
  }
}

リポジトリ
https://github.com/s-sasaki-0529/tiny-package-manager

shingo.sasakishingo.sasaki
$ npm install commander

以下の感じのコマンドラインオプションを定義する

  • install: package.json に基づいてインストール
  • install --production: package.json に基づいて、 dependencies のみインストール
  • install axios vue: axios vuedependencies に追加しつつインストール
  • install typescript --save-dev: typescriptdevDependencies に追加しつつインストール
shingo.sasakishingo.sasaki
src/cli.ts
import { Command } from 'commander'

const program = new Command()

program
  .command('install')
  .argument('[packageNames...]')
  .action(packageNames => {
    const options = program.opts()
    console.log({ command: 'install', packageNames, options })
  })

program
  .option('--production', 'devDependencies のインストールを省略する')
  .option('--save-dev', 'パッケージを devDependencies に追加する')
  .parse()

コマンド、サブコマンド、オプションを渡せてることを確認。

$ ts-node src/cli.ts install axios vue --save-dev --production
{
  command: 'install',
  packageNames: [ 'axios', 'vue' ],
  options: { saveDev: true, production: true }
}

ヘルプは勝手に作られる

$ ts-node src/cli.ts --help
Usage: cli [options] [command]

Options:
  --production               devDependencies のインストールを省略する
  --save-dev                 パッケージを devDependencies に追加する
  -h, --help                 display help for command

Commands:
  install [packageNames...]
  help [command]             display help for command
shingo.sasakishingo.sasaki

package.json とその内容を読み書きするモジュールを用意したい。
dependencies モジュールでいいか。

shingo.sasakishingo.sasaki

一旦型だけ整理してみた。

src/types.d.ts
type PackageName = string
type Version = string

type DependenciesMap = {
  [name: PackageName]: Version
}

type PackageJson = {
  dependencies: DependenciesMap
  devDependencies: DependenciesMap
}

type LockFile = {
  [dependency: `${PackageName}@${Version}`]: {
    version: Version
    url: string
    shasum: string
    dependencies: {
      [name: PackageName]: Version
    }
  }
}

キャッシュとかそういうのもあった気がするけど、一旦はこれでヨシとする。

shingo.sasakishingo.sasaki

package.json の読み書きを行う packageJson モジュールを用意して、package.json から PackageJson 型のデータを抜き出す関数を定義してく。

$ npm install find-up
src/packageJson.ts
import { findUp } from 'find-up'

export async function findPackageJsonPath() {
  return findUp('package.json')
}

findPackageJsonPath().then(path => {
  console.log(path)
})

正しい動作確認のやり方がわからないけど、とりあえず dev ディレクトリを切って package.json を作成して、そのディレクトリからスクリプトを実行したらよい感じに取れてたのでOK.

dev/package.json
{
  "name": "dev",
  "version": "1.0.0",
  "dependencies": {
    "axios": "^0.18.0",
    "dayjs": "^1.0.0"
  },
  "devDependencies": {
    "chalk": "^5.0.0"
  }
}
$ node ../dist/src/packageJson.js 
/Users/shingo.sasaki/tiny-package-manager/dev/package.json
shingo.sasakishingo.sasaki

package.json を読み込んで、dependencies と devDependencies を抜き出していく。

src/packageJson.ts
export async function parsePackageJson(path: string): Promise<PackageJson> {
  return new Promise(resolve => {
    readFile(path)
      .then(data => {
        const json = JSON.parse(data.toString())
        resolve({
          dependencies: json.dependencies,
          devDependencies: json.devDependencies
        })
      })
      .catch(() => {
        resolve({
          dependencies: {},
          devDependencies: {}
        })
      })
  })
}

ちょっとごちゃっとした。無駄に Promise ネストしてる気がするけど、一旦コレで以下は満たせた。

  • package.json があれば、 dependencies / devDependencies を抜き出す
  • package.json がなければ dependencies / devDependencies が空のオブジェクトを返す

自身の package.json に対して実行してみるとこんな感じに。

$ node dist/src/packageJson.js 
{
  dependencies: { commander: '^9.4.0', 'find-up': '^6.3.0' },
  devDependencies: {
    '@types/node': '^18.7.13',
    '@typescript-eslint/eslint-plugin': '^5.35.1',
    '@typescript-eslint/parser': '^5.35.1',
    eslint: '^8.23.0',
    prettier: '^2.7.1',
    typescript: '^4.8.2'
  }
}
shingo.sasakishingo.sasaki

npm リポジトリ周りの型を整理しよう。
これ公開されてる型パッケージある気がするけど、今回は自分で用意してみる。

コレを参考に
https://registry.npmjs.org/axios

使うとこだけ抜き出す。

src/types.d.ts
type NpmManifest = {
  name: PackageName
  'dist-tags': {
    latest: Version
  }
  versions: {
    [version: Version]: {
      dist: {
        tarball: string
        shasum: string
      }
      dependencies: DependenciesMap
    }
  }
}
shingo.sasakishingo.sasaki

パッケージ名から npm リポジトリのマニフェストを取り出す関数

src/npm.ts
export async function fetchPackageManifest(name: PackageName): Promise<NpmManifest> {
  const manifestUrl = `${REPOSITORY_URL}/${name}`
  const manifest = (await fetch(manifestUrl).then(res => res.json())) as NpmManifest
  return manifest
}

マニフェストを元にパッケージをダウンロードする関数

src/npm.ts
const DEFAULT_DOWNLOAD_DIR = `${process.cwd()}/node_modules`

export async function savePackageTarball(manifest: NpmManifest, version: Version, dir = DEFAULT_DOWNLOAD_DIR) {
  const tarballUrl = manifest.versions[version].dist.tarball
  const path = `${dir}/${manifest.name}`
  mkdir(path, { recursive: true })

  const tarResponse = await fetch(tarballUrl)
  return tarResponse.body?.pipe(tar.extract({ cwd: path, strip: 1 }))
}

マニフェストから対象バージョンの tar URL を抜き出してダウンロード。
tar を解凍して node_modules ディレクトリに保存する手順は元コードをコピペしただけで良くわかってない。

shingo.sasakishingo.sasaki

次にやることメモ

  • package.json からパッケージリストを抜き出して、全てダウンロードできるようにする
  • 依存パッケージを再帰的にダウンロードできるようにする

キャッシュとかバージョン衝突とかはまた次のステップでOK

shingo.sasakishingo.sasaki

ここまで作ったモジュールを使って、ざーっと書いた。

できてるもの

  • package.json に含まれている依存パッケージをダウンロードできる
  • コマンドラインオプションからパッケージを指定してダウンロードできる
  • --production オプションで devDependencies のダウンロードを省略できる

できてなくて実現したいもの

  • 依存パッケージが依存するパッケージの再帰的なダウンロード
  • 最新バージョンでなく、指定したセマンティックバージョン指定でのダウンロード
  • package.json の更新
  • lock ファイルの作成
  • 既にダウンロード済みのパッケージをスキップ
src/tinyPm.ts
type InstallOption = {
  saveDev?: boolean
  production?: boolean
}

export async function install(packageNames: PackageName[], option: InstallOption = {}) {
  // インストール対象パッケージ一覧を初期化
  const dependencyMap: PackageDependencyMap = {
    dependencies: {},
    devDependencies: {}
  }

  // package.json を探索し、存在する場合はパッケージ依存関係を読み込む
  const packageJsonPath = await findPackageJsonPath()
  if (packageJsonPath) {
    const packageJson = await parsePackageJson(packageJsonPath)
    dependencyMap.dependencies = packageJson.dependencies
    dependencyMap.devDependencies = packageJson.devDependencies
  }

  // 追加インストールするパッケージを dependencies または devDependencies に追加する
  packageNames.forEach(packageName => {
    if (option.saveDev) {
      dependencyMap.devDependencies[packageName] = ''
    } else {
      dependencyMap.dependencies[packageName] = ''
    }
  })

  // production の指定がある場合は devDependencies のインストールを省略する
  if (option.production) {
    dependencyMap.devDependencies = {}
  }

  // npm リポジトリから各パッケージのインストールを行う
  // devDependencies については production オプションがある場合は省略
  const installPromises = []
  installPromises.push(
    Object.keys(dependencyMap.dependencies).map(packageName => {
      return savePackageTarball(packageName, '') // TODO: latest でなく指定されたバージョンを使う
    })
  )
  if (!option.production) {
    installPromises.push(
      Object.keys(dependencyMap.devDependencies).map(packageName => {
        return savePackageTarball(packageName, '') // TODO: latest でなく指定されたバージョンを使う
      })
    )
  }
  return Promise.all(installPromises)
}
shingo.sasakishingo.sasaki

セマンティックバージョンの指定からインストール対象のバージョンを特定するのはどうすればいいんだろ。
元ソースを調べてみる。

これ使ってるみたい。
https://github.com/npm/node-semver

マニフェストから取り出したバージョン一覧に対して、指定されたバージョン式を満たす最大のバージョンを返してる。
https://github.com/g-plane/tiny-package-manager/blob/4e2602875967205a2d9115e76955fe975297b96a/src/list.ts#L59-L61

shingo.sasakishingo.sasaki

こんな感じに、バージョン制約がある場合はその中で最新を、ない場合は最新をインストールするように出来た。

src/npm.ts
export async function savePackageTarball(name: PackageName, vc?: VersionConstraint, dir = DEFAULT_DOWNLOAD_DIR) {
  const manifest = await fetchPackageManifest(name)
  const versions = Object.keys(manifest.versions)
  const version: Version = vc ? semver.maxSatisfying(versions, vc) : manifest['dist-tags'].latest

  const path = `${dir}/${manifest.name}`
  mkdir(path, { recursive: true })

  const tarballUrl = manifest.versions[version].dist.tarball
  const tarResponse = await fetch(tarballUrl)
  return tarResponse.body?.pipe(tar.extract({ cwd: path, strip: 1 }))
}
package.json
{
  "name": "dev",
  "version": "1.0.0",
  "dependencies": {
    "axios": "0.2.0",
    "dayjs": "^1.0.0"
  },
  "devDependencies": {
    "chalk": "^4.0.0"
  }
}

の場合は

  • axios 0.2.0
  • dayjs 1.11.5
  • chalk 4.1.2

が入ることを確認

shingo.sasakishingo.sasaki

依存ツリーに含まれるパッケージをまとめてインストールできるようにしたいけど、ぼちぼち動作確認が大変になってきたから、ログ出力を仕込みたい。

上記の package.json の場合は以下みたいなログが出ると良いかな。

axios@0.2.0 resolve to 0.2.0
dayjs@^1.0.0 resolve to 1.11.5
chalk@^4.0.0 resolve to 4.1.2

みたいなの出せればいいかな。

shingo.sasakishingo.sasaki
src/logger.ts
export function resolveLog(name: PackageName, vc: VersionConstraint, version: Version) {
  console.log(`${name}@${vc} resolve to ${version}`)
}
$ node ../dist/src/cli.js install vue
dayjs@^1.0.0 resolve to 1.11.5
chalk@^4.0.0 resolve to 4.1.2
axios@0.2.0 resolve to 0.2.0
vue@* resolve to 3.2.37

良さげかな

shingo.sasakishingo.sasaki

続けて、依存ツリーに含まれるパッケージを一括インストールするのを頑張ってみる。

元パッケージのコードを参考にしよう。

一旦パッケージの衝突 (異なるバージョンが別経路で要求された場合) は考えないで、 axios のインストールにだけ集中してみよう。

shingo.sasakishingo.sasaki

ごりっと再帰的にパッケージを取得し続けるだけのコードから書いてみた。

src/resolver.ts
export async function collectDepsPackageList(
  packageName: PackageName,
  vc: VersionConstraint,
  packageList: Readonly<DependenciesMap>
) {
  // 破壊的変更を避けるために新しいパッケージ一覧を複製しておく
  let newPackageList = { ...packageList }

  // パッケージのマニフェストを取得する
  const manifest = await fetchPackageManifest(packageName)

  // バージョン制約から最新のバージョンを抜き出す
  const version = semver.maxSatisfying(Object.keys(manifest.versions), vc)
  if (!manifest.versions[version]) return packageList

  // インストールリストに自身を追加する
  newPackageList[packageName] = version
  resolveLog(packageName, vc, version)

  // 依存するパッケージに対して再帰的に同様の操作を行う
  if (manifest.versions[version].dependencies) {
    for (const [depName, depVersion] of Object.entries(manifest.versions[version].dependencies)) {
      newPackageList = await collectDepsPackageList(depName, depVersion, packageList)
    }
  }

  return newPackageList
}

試しに vue のインストールをすると良い感じ。同じようなパッケージへの依存を繰り返してるからキャッシュは重要だ。

$ node ../dist/src/cli.js install vue
vue@* resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/runtime-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/runtime-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/reactivity@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
csstype@^2.6.8 resolve to 2.6.20
@vue/compiler-sfc@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/compiler-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/compiler-ssr@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/reactivity-transform@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/shared@3.2.37 resolve to 3.2.37
estree-walker@^2.0.2 resolve to 2.0.2
magic-string@^0.25.7 resolve to 0.25.9
sourcemap-codec@^1.4.8 resolve to 1.4.8
@vue/shared@3.2.37 resolve to 3.2.37
estree-walker@^2.0.2 resolve to 2.0.2
magic-string@^0.25.7 resolve to 0.25.9
sourcemap-codec@^1.4.8 resolve to 1.4.8
source-map@^0.6.1 resolve to 0.6.1
postcss@^8.1.10 resolve to 8.4.16
nanoid@^3.3.4 resolve to 3.3.4
picocolors@^1.0.0 resolve to 1.0.0
source-map-js@^1.0.2 resolve to 1.0.2
@vue/server-renderer@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-ssr@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
shingo.sasakishingo.sasaki

雑にマニフェストのキャッシュ取るような構造にしたら一気に高速化した。

src/manifest.ts
const MANIFEST_CACHE: Record<PackageName, NpmManifest> = {}

export async function fetchPackageManifest(name: PackageName): Promise<NpmManifest> {
  if (!MANIFEST_CACHE[name]) {
    const manifestUrl = `${REPOSITORY_URL}/${name}`
    const manifest = (await fetch(manifestUrl).then(res => res.json())) as NpmManifest
    MANIFEST_CACHE[name] = manifest
  }
  return MANIFEST_CACHE[name]
}
shingo.sasakishingo.sasaki

一応この時点で、(バージョンコンフリクトがない限りは) 必要な全てのパッケージのインストールができるのかな。

実用的な例で試してみる。

package.json
{
  "name": "dev",
  "version": "1.0.0",
  "dependencies": {
    "vue": "^3.2.37"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^3.0.1",
    "@vue/tsconfig": "^0.1.3",
    "typescript": "~4.7.4",
    "vite": "^3.0.4",
    "vue-tsc": "^0.39.5"
  }
}

vite で Vue アプリ書く際の単純な構成。
ミニマムなコード書く。

App.vue
<script setup lang="ts">
import { ref } from 'vue'

const inputValue = ref<string>('')
</script>

<template>
  <div>
    <input v-model="inputValue" />
    <p>{{ inputValue }}</p>
  </div>
</template>

npm で動作確認

$ npm install
$ node_modules/vite/bin/vite.js 

サーバーが起動してアプリケーションが動くことを確認。
これを自作パッケージマネージャで動かすことを目標にしてみよう。

shingo.sasakishingo.sasaki

インストール時のログを追加して、 node_modules にダウンロードが完了したら吐くようにする

src/logger.ts
export function installLog(name: PackageName, version: Version) {
  console.log(`${name}@${version} installed`)
}

依存解決した各パッケージをインストールするコードを復活

src/tinyPm.ts
  // npm リポジトリから各パッケージのインストールを行う
  const installPromises = []
  installPromises.push(
    Object.keys(fullDependenciesMap).map(packageName => {
      return savePackageTarball(packageName, fullDependenciesMap[packageName])
    })
  )
  return Promise.all(installPromises)
shingo.sasakishingo.sasaki

いっけー

$ node ../dist/cli.js install
vue@^3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/runtime-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/runtime-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/reactivity@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
csstype@^2.6.8 resolve to 2.6.20
@vue/compiler-sfc@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/compiler-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/compiler-ssr@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/reactivity-transform@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/shared@3.2.37 resolve to 3.2.37
estree-walker@^2.0.2 resolve to 2.0.2
magic-string@^0.25.7 resolve to 0.25.9
sourcemap-codec@^1.4.8 resolve to 1.4.8
@vue/shared@3.2.37 resolve to 3.2.37
estree-walker@^2.0.2 resolve to 2.0.2
magic-string@^0.25.7 resolve to 0.25.9
sourcemap-codec@^1.4.8 resolve to 1.4.8
source-map@^0.6.1 resolve to 0.6.1
postcss@^8.1.10 resolve to 8.4.16
nanoid@^3.3.4 resolve to 3.3.4
picocolors@^1.0.0 resolve to 1.0.0
source-map-js@^1.0.2 resolve to 1.0.2
@vue/server-renderer@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-ssr@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vitejs/plugin-vue@^3.0.1 resolve to 3.0.3
@vue/tsconfig@^0.1.3 resolve to 0.1.3
typescript@~4.7.4 resolve to 4.7.4
vite@^3.0.4 resolve to 3.0.9
esbuild@^0.14.47 resolve to 0.14.54
@esbuild/linux-loong64@0.14.54 resolve to 0.14.54
esbuild-android-64@0.14.54 resolve to 0.14.54
esbuild-android-arm64@0.14.54 resolve to 0.14.54
esbuild-darwin-64@0.14.54 resolve to 0.14.54
esbuild-darwin-arm64@0.14.54 resolve to 0.14.54
esbuild-freebsd-64@0.14.54 resolve to 0.14.54
esbuild-freebsd-arm64@0.14.54 resolve to 0.14.54
esbuild-linux-32@0.14.54 resolve to 0.14.54
esbuild-linux-64@0.14.54 resolve to 0.14.54
esbuild-linux-arm@0.14.54 resolve to 0.14.54
esbuild-linux-arm64@0.14.54 resolve to 0.14.54
esbuild-linux-mips64le@0.14.54 resolve to 0.14.54
esbuild-linux-ppc64le@0.14.54 resolve to 0.14.54
esbuild-linux-riscv64@0.14.54 resolve to 0.14.54
esbuild-linux-s390x@0.14.54 resolve to 0.14.54
esbuild-netbsd-64@0.14.54 resolve to 0.14.54
esbuild-openbsd-64@0.14.54 resolve to 0.14.54
esbuild-sunos-64@0.14.54 resolve to 0.14.54
esbuild-windows-32@0.14.54 resolve to 0.14.54
esbuild-windows-64@0.14.54 resolve to 0.14.54
esbuild-windows-arm64@0.14.54 resolve to 0.14.54
postcss@^8.4.16 resolve to 8.4.16
nanoid@^3.3.4 resolve to 3.3.4
picocolors@^1.0.0 resolve to 1.0.0
source-map-js@^1.0.2 resolve to 1.0.2
resolve@^1.22.1 resolve to 1.22.1
is-core-module@^2.9.0 resolve to 2.10.0
has@^1.0.3 resolve to 1.0.3
function-bind@^1.1.1 resolve to 1.1.1
path-parse@^1.0.7 resolve to 1.0.7
supports-preserve-symlinks-flag@^1.0.0 resolve to 1.0.0
rollup@>=2.75.6 <2.77.0 || ~2.77.0 resolve to 2.77.3
fsevents@~2.3.2 resolve to 2.3.2
fsevents@~2.3.2 resolve to 2.3.2
vue-tsc@^0.39.5 resolve to 0.39.5
@volar/vue-language-core@0.39.5 resolve to 0.39.5
@volar/code-gen@0.39.5 resolve to 0.39.5
@volar/source-map@0.39.5 resolve to 0.39.5
@volar/source-map@0.39.5 resolve to 0.39.5
@volar/vue-code-gen@0.39.5 resolve to 0.39.5
@volar/code-gen@0.39.5 resolve to 0.39.5
@volar/source-map@0.39.5 resolve to 0.39.5
@volar/source-map@0.39.5 resolve to 0.39.5
@vue/compiler-core@^3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/compiler-dom@^3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/shared@^3.2.37 resolve to 3.2.37
@vue/compiler-sfc@^3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/compiler-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/compiler-ssr@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/reactivity-transform@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/shared@3.2.37 resolve to 3.2.37
estree-walker@^2.0.2 resolve to 2.0.2
magic-string@^0.25.7 resolve to 0.25.9
sourcemap-codec@^1.4.8 resolve to 1.4.8
@vue/shared@3.2.37 resolve to 3.2.37
estree-walker@^2.0.2 resolve to 2.0.2
magic-string@^0.25.7 resolve to 0.25.9
sourcemap-codec@^1.4.8 resolve to 1.4.8
source-map@^0.6.1 resolve to 0.6.1
postcss@^8.1.10 resolve to 8.4.16
nanoid@^3.3.4 resolve to 3.3.4
picocolors@^1.0.0 resolve to 1.0.0
source-map-js@^1.0.2 resolve to 1.0.2
@vue/reactivity@^3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@volar/vue-typescript@0.39.5 resolve to 0.39.5
@volar/code-gen@0.39.5 resolve to 0.39.5
@volar/source-map@0.39.5 resolve to 0.39.5
@volar/typescript-faster@0.39.5 resolve to 0.39.5
semver@^7.3.7 resolve to 7.3.7
lru-cache@^6.0.0 resolve to 6.0.0
yallist@^4.0.0 resolve to 4.0.0
@volar/vue-language-core@0.39.5 resolve to 0.39.5
@volar/code-gen@0.39.5 resolve to 0.39.5
@volar/source-map@0.39.5 resolve to 0.39.5
@volar/source-map@0.39.5 resolve to 0.39.5
@volar/vue-code-gen@0.39.5 resolve to 0.39.5
@volar/code-gen@0.39.5 resolve to 0.39.5
@volar/source-map@0.39.5 resolve to 0.39.5
@volar/source-map@0.39.5 resolve to 0.39.5
@vue/compiler-core@^3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/compiler-dom@^3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/shared@^3.2.37 resolve to 3.2.37
@vue/compiler-sfc@^3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/compiler-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/compiler-ssr@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-dom@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/reactivity-transform@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
@vue/compiler-core@3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
@babel/parser@^7.16.4 resolve to 7.18.13
estree-walker@^2.0.2 resolve to 2.0.2
source-map@^0.6.1 resolve to 0.6.1
@vue/shared@3.2.37 resolve to 3.2.37
estree-walker@^2.0.2 resolve to 2.0.2
magic-string@^0.25.7 resolve to 0.25.9
sourcemap-codec@^1.4.8 resolve to 1.4.8
@vue/shared@3.2.37 resolve to 3.2.37
estree-walker@^2.0.2 resolve to 2.0.2
magic-string@^0.25.7 resolve to 0.25.9
sourcemap-codec@^1.4.8 resolve to 1.4.8
source-map@^0.6.1 resolve to 0.6.1
postcss@^8.1.10 resolve to 8.4.16
nanoid@^3.3.4 resolve to 3.3.4
picocolors@^1.0.0 resolve to 1.0.0
source-map-js@^1.0.2 resolve to 1.0.2
@vue/reactivity@^3.2.37 resolve to 3.2.37
@vue/shared@3.2.37 resolve to 3.2.37
typescript@4.7.4 installed
vite@3.0.9 installed
source-map@0.6.1 installed
vue@3.2.37 installed
estree-walker@2.0.2 installed
vue-tsc@0.39.5 installed
esbuild-freebsd-64@0.14.54 installed
sourcemap-codec@1.4.8 installed
magic-string@0.25.9 installed
esbuild-linux-mips64le@0.14.54 installed
esbuild@0.14.54 installed
esbuild-linux-ppc64le@0.14.54 installed
csstype@2.6.20 installed
picocolors@1.0.0 installed
esbuild-android-64@0.14.54 installed
nanoid@3.3.4 installed
esbuild-linux-32@0.14.54 installed
esbuild-windows-32@0.14.54 installed
esbuild-sunos-64@0.14.54 installed
@vue/compiler-sfc@3.2.37 installed
esbuild-darwin-arm64@0.14.54 installed
@vue/compiler-ssr@3.2.37 installed
@vue/shared@3.2.37 installed
@vue/compiler-core@3.2.37 installed
esbuild-linux-riscv64@0.14.54 installed
postcss@8.4.16 installed
esbuild-darwin-64@0.14.54 installed
@vue/runtime-core@3.2.37 installed
@babel/parser@7.18.13 installed
esbuild-freebsd-arm64@0.14.54 installed
has@1.0.3 installed
is-core-module@2.10.0 installed
path-parse@1.0.7 installed
yallist@4.0.0 installed
semver@7.3.7 installed
resolve@1.22.1 installed
esbuild-windows-64@0.14.54 installed
esbuild-netbsd-64@0.14.54 installed
esbuild-linux-s390x@0.14.54 installed
esbuild-linux-arm@0.14.54 installed
@volar/code-gen@0.39.5 installed
@volar/vue-language-core@0.39.5 installed
@vue/tsconfig@0.1.3 installed
@esbuild/linux-loong64@0.14.54 installed
@vue/server-renderer@3.2.37 installed
@vitejs/plugin-vue@3.0.3 installed
@volar/vue-code-gen@0.39.5 installed
@volar/vue-typescript@0.39.5 installed
@volar/source-map@0.39.5 installed
@vue/runtime-dom@3.2.37 installed
@vue/compiler-dom@3.2.37 installed
@volar/typescript-faster@0.39.5 installed
function-bind@1.1.1 installed
supports-preserve-symlinks-flag@1.0.0 installed
lru-cache@6.0.0 installed
source-map-js@1.0.2 installed
fsevents@2.3.2 installed
@vue/reactivity@3.2.37 installed
@vue/reactivity-transform@3.2.37 installed
esbuild-openbsd-64@0.14.54 installed
esbuild-android-arm64@0.14.54 installed
esbuild-windows-arm64@0.14.54 installed
esbuild-linux-arm64@0.14.54 installed
rollup@2.77.3 installed
esbuild-linux-64@0.14.54 installed

いけた。 Vite 起動してアプリケーション動いたぞい。

shingo.sasakishingo.sasaki

とりあえず書き込み用オブジェクトに追加する関数を用意

src/lock.ts
/**
 * 更新用の lock ファイルの内容で書き込み用
 */
const newLockFile: LockFile = {}

export async function addLockFile(packageName: PackageName, vc: VersionConstraint, version: Version) {
  const manifest = await fetchPackageManifest(packageName)
  const info = manifest.versions[version]
  newLockFile[`${packageName}@${vc}`] = {
    version,
    url: info.dist.tarball,
    shasum: info.dist.shasum,
    dependencies: info.dependencies || {}
  }
}

パッケージのインストール時に lock ファイルに書き込む (ここでやることかは微妙なので整理が必要そう)

src/npm.ts
export async function savePackageTarball(name: PackageName, vc: VersionConstraint = '*') {
  const manifest = await fetchPackageManifest(name)
  const versions = Object.keys(manifest.versions)
  const version = semver.maxSatisfying(versions, vc)

  const path = `${DEFAULT_DOWNLOAD_DIR}/${manifest.name}`
  mkdir(path, { recursive: true })

  const tarballUrl = manifest.versions[version].dist.tarball
  const tarResponse = await fetch(tarballUrl)
  tarResponse.body?.pipe(tar.extract({ cwd: path, strip: 1 }))

  addLockFile(name, vc, version) // 追加
  installLog(name, version)
}

全行程完了時に、lock ファイルを書き込めるようにする

src/lock.ts
export async function writeLockFile() {
  const lockFileJson = JSON.stringify(newLockFile, null, 2)
  return writeFile(LOCK_FILE_PATH, lockFileJson)
}

lock ファイルが出来上がる!

tiny-pm.lock
{
  "estree-walker@2.0.2": {
    "version": "2.0.2",
    "url": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
    "shasum": "52f010178c2a4c117a7757cfe942adb7d2da4cac",
    "dependencies": {}
  },
  "sourcemap-codec@1.4.8": {
    "version": "1.4.8",
    "url": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
    "shasum": "ea804bd94857402e6992d05a38ef1ae35a9ab4c4",
    "dependencies": {}
  },
  "csstype@2.6.20": {
    "version": "2.6.20",
    "url": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
    "shasum": "9229c65ea0b260cf4d3d997cb06288e36a8d6dda",
    "dependencies": {}
  },
  "postcss@8.4.16": {
    "version": "8.4.16",
    "url": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
    "shasum": "33a1d675fac39941f5f445db0de4db2b6e01d43c",
    "dependencies": {
      "nanoid": "^3.3.4",
      "picocolors": "^1.0.0",
      "source-map-js": "^1.0.2"
    }
  },
  // 以下略
}

あれ、キーにはバージョン制約が入ってほしいのにバージョンが入ってる。
どっかでミスってるので修正しよう。

shingo.sasakishingo.sasaki

lock ファイルがあることで、manifest の取得とバージョン解決を省略できるようになるってことでいいのかな。複数人開発の場合は、メンバー間で厳密なバージョンの固定ができるようになって環境差異が生まれなくなると。

shingo.sasakishingo.sasaki

一応 npm ドキュメントでロックファイルの目的を再確認

https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-json

  • メンバー、デプロイ、CIそれぞれで常に同一の依存ツリーに基づいてインストールできるようにすること
  • node_modules をコミットすることなく、その時点の依存ツリーを履歴として残せるようにすること
  • 依存ツリーの変更時に、変更差分を可視化出来るようにすること
  • パッケージインストール前のバージョン解決をスキップできること

今回のパッケージマネージャー自作の目的としてはやっぱり最後のヤツだけで良さそうかな。

shingo.sasakishingo.sasaki

npm install 時にパッケージのコンフリクトがない場合は node_modules 直下にだけ配置して、コンフリクトがある場合は node_moduels 以下の各パッケージディレクトリにさらに node_modules が掘られてるのか。

で、各パッケージは同ディレクトリから上に向かってモジュールを探索するって感じか。npm install 時にパッケージのコンフリクトがない場合は node_modules 直下にだけ配置して、コンフリクトがある場合は node_moduels 以下の各パッケージディレクトリにさらに node_modules が掘られてるのか。

で、各パッケージは同ディレクトリから上に向かってモジュールを探索するって感じか。

コンフリクト制御が大変そうだけど、一旦それはなしで実装進める。

shingo.sasakishingo.sasaki

lock ファイルができてるので、パッケージの依存解決時に、lock ファイルに記載があれば manifest の取得及びバージョンの決定をスキップできるようになった。むしろちゃんとスキップして固定バージョンを利用し続けなければならない。

shingo.sasakishingo.sasaki

ファイルを読み込む関数を用意しておく。

src/lock.ts
export async function readLockFile() {
  const buffer = await readFile(LOCK_FILE_PATH, 'utf8').catch(e => {
    if (e.code === 'ENOENT') return '{}'
    throw e
  })
  const lockFile = JSON.parse(buffer) as LockFile
  Object.assign(currentLockFile, lockFile)
  return lockFile
}
shingo.sasakishingo.sasaki

型をちょっと整理。
命名が雑になってきてる。

// 解決済みのパッケージ情報
type LockedPackageInfo = {
  version: Version
  url: string
  shasum: string
  dependencies: DependenciesMap
}

// 解決済みパッケージ情報一覧(≒ tiny-pm.lock)
type LockFile = {
  [dependency: `${PackageName}@${VersionConstraint}`]: LockedPackageInfo
}
shingo.sasakishingo.sasaki

lock ファイルから該当のパッケージ情報があれば返す。
ない場合ここでは NULL を返して、呼び出し側のほうでマニフェストの取得と lock ファイルへの追加をやってもらおう。

src/lock.ts
export function readLockedPackageInfo(name: PackageName, vc: VersionConstraint): LockedPackageInfo | null {
  const packageInfo = currentLockFile[`${name}@${vc}`]
  if (packageInfo) {
    return {
      version: packageInfo.version,
      url: packageInfo.url,
      shasum: packageInfo.shasum,
      dependencies: packageInfo.dependencies
    }
  } else {
    return null
  }
}
shingo.sasakishingo.sasaki

lock ファイルを効率的に活用するの難しいな。
一旦バージョン制約を満たすかじゃなくて、バージョン制約文字列が完全一致かだけで見てみよう。

shingo.sasakishingo.sasaki

下手に並列化するとデバッグが大変なので直接で割り切ろう。そういうパフォーマンス改善はあとあと。

shingo.sasakishingo.sasaki

Lock ファイルまたは manifest を使って依存解決を行う関数を切り出す

src/resolver.ts
async function resolvePackage(packageName: PackageName, vc: VersionConstraint): Promise<LockedPackageInfo | null> {
  // Lock ファイルに既に情報があればそれを利用する
  const lockedPackageInfo = readLockedPackageInfo(packageName, vc)
  if (lockedPackageInfo) return lockedPackageInfo

  // Lock ファイルに情報がなければ、パッケージマニフェストから取得する
  // TODO: ここで Lock ファイルの更新も必要?
  const manifest = await fetchPackageManifest(packageName)
  const version = semver.maxSatisfying(Object.keys(manifest.versions), vc)
  if (!manifest.versions[version]) return null

  resolveLog(packageName, vc, version)
  return {
    version,
    url: manifest.versions[version].dist.tarball,
    shasum: manifest.versions[version].dist.shasum,
    dependencies: manifest.versions[version].dependencies || {}
  }
}

これで Lock ファイルがある場合はマニフェストを参照することなくダウンロードできるようになった。

shingo.sasakishingo.sasaki

ログまわりがわかりづらくなってたので、プレフィックス付きで整理しよう。
以下のイベントについてのログを出せるようにする。

初回

$ node ../dist/cli.js install
[Resolve by manifest] axios@^0.27.0 to 0.27.2
[Resolve by manifest] follow-redirects@^1.14.9 to 1.15.1
[Resolve by manifest] form-data@^4.0.0 to 4.0.0
[Resolve by manifest] asynckit@^0.4.0 to 0.4.0
[Resolve by manifest] combined-stream@^1.0.8 to 1.0.8
[Resolve by manifest] delayed-stream@~1.0.0 to 1.0.0
[Resolve by manifest] mime-types@^2.1.12 to 2.1.35
[Resolve by manifest] mime-db@1.52.0 to 1.52.0
[Installed] axios@0.27.2
[Installed] follow-redirects@1.15.1
[Installed] form-data@4.0.0
[Installed] asynckit@0.4.0
[Installed] combined-stream@1.0.8
[Installed] delayed-stream@1.0.0
[Installed] mime-types@2.1.35
[Installed] mime-db@1.52.0

lockファイル生成後

$ node ../dist/cli.js install
[Resolve by lockfile] axios@^0.27.0 to 0.27.2
[Resolve by lockfile] follow-redirects@^1.14.9 to 1.15.1
[Resolve by lockfile] form-data@^4.0.0 to 4.0.0
[Resolve by lockfile] asynckit@^0.4.0 to 0.4.0
[Resolve by lockfile] combined-stream@^1.0.8 to 1.0.8
[Resolve by lockfile] delayed-stream@~1.0.0 to 1.0.0
[Resolve by lockfile] mime-types@^2.1.12 to 2.1.35
[Resolve by lockfile] mime-db@1.52.0 to 1.52.0
[Installed] axios@0.27.2
[Installed] follow-redirects@1.15.1
[Installed] form-data@4.0.0
[Installed] asynckit@0.4.0
[Installed] combined-stream@1.0.8
[Installed] delayed-stream@1.0.0
[Installed] mime-types@2.1.35
[Installed] mime-db@1.52.0

node_modules の内容が lock ファイルで定義されている前提なら、lock ファイルから取り出したパッケージはダウンロード不要なのかな。

shingo.sasakishingo.sasaki

いやそれは嘘か。各環境で新たにインストールする際に lock ファイルを参考にするんだから、そこは対になってない。

shingo.sasakishingo.sasaki

でも npm でも2回目以降はすぐにインストール完了するから、 node_modules 内の何かは見てるのか。

shingo.sasakishingo.sasaki

ここまで雰囲気で依存解決の実装しちゃってたけど、元パッケージとは実装方法が違うので、もう少し深ぼってく。

おそらくはバージョンのコンフリクトへの対応で必要になるからこういう実装なんだろうな。

https://github.dev/g-plane/tiny-package-manager

shingo.sasakishingo.sasaki

元パッケージの実装

登場人物

  • topLevel: node_modules 直下にインストールするパッケージ一覧
  • unsatisfied: topLevel とバージョンコンフリクトを起こしそうなので、各パッケージの node_modules 以下にインストールするパッケージ

パッケージA について解決する流れ

  • Lock ファイルまたは npm リポジトリからAのマニフェストを取得
  • マニフェストを元に、バージョン制約を満たす最新バージョンを決定する
  • Aが
    • topLevel に存在しない場合
      • topLevel に追加する
    • topLevel に存在するが、バージョン制約を満たしている場合
      • dependencies 同士でのコンフリクトがある場合
        • 解決不要なので終了
      • dependencies 同士でのコンフリクトがある場合
        • unsatisfied に追加する
    • topLevel に存在し、かつバージョン制約も満たしていない場合
      • unsatisfied に追加する
  • A の解決したバージョンを Lock ファイルに記述する
  • A に dependencies があれば
    • stack に自身を積む
    • 各dependencies のうち、循環参照が発生していなければ、同様の解決処理を呼び出す(再帰)
    • stack から自身を外す

めちゃくちゃ難しい。
ここで考慮してるケースのうち一部でもサポートできれば一旦OKかもしれない。

shingo.sasakishingo.sasaki

↑の実装はラスボスとして、先に package.json を書き換える処理から入ろうかな。

shingo.sasakishingo.sasaki

package.json の更新はこれで充分そうかな。

src/package.json
/**
 * package.json を依存関係オブジェクトを用いて書き換える
 */
export async function writePackageJson(path: string, dependencyMap: PackageDependencyMap) {
  const data = await readFile(path)
  const currentJson = JSON.parse(data.toString())
  const newJson = {
    ...currentJson,
    dependencies: dependencyMap.dependencies,
    devDependencies: dependencyMap.devDependencies
  }
  if (Object.keys(newJson.dependencies).length === 0) delete newJson.dependencies
  if (Object.keys(newJson.devDependencies).length === 0) delete newJson.devDependencies
  return writeFile(path, JSON.stringify(newJson, null, 2))
}

追加分だけじゃなくて更新後の状態をまるごと渡す形にすることで、今回対応するかはわからないけどパッケージの削除にも使えるようになる。

shingo.sasakishingo.sasaki

ややこしくなるから、ここでは package.json は必ず存在することにしよう。

src/packageJson.ts
/**
 * package.json を探索し、そのパスを返す
 */
export async function findPackageJsonPath() {
  const path = await findUp('package.json')
  if (!path) throw new Error('package.json not found')
  return path
}
shingo.sasakishingo.sasaki
 node ../dist/cli.js install axios
 node ../dist/cli.js install typescript --save-dev

package.json
{
  "name": "dev",
  "version": "1.0.0",
  "dependencies": {
    "axios": "*"
  },
  "devDependencies": {
    "typescript": "*"
  }
}

が生成されるようになったけど、* じゃためよね。 * が来たら最新版にあわせたバージョン制約作らなきゃ。

shingo.sasakishingo.sasaki

ちょっとゴチャッたけど、 install コマンドでパッケージを追加する場合は resolver で最新バージョンを取得して、それを用いたバージョン制約に書き換えるように。

  // 追加インストールするパッケージを dependencies または devDependencies に追加する
  // バージョン指定がない場合は、最新バージョンを確認してそれを使用する
  // TODO: バージョン指定がある場合の対応
  for (const packageName of packageNames) {
    const latestPackageInfo = await resolvePackage(packageName, '*')
    const latestVersion = latestPackageInfo?.version
    if (!latestVersion) throw new Error(`Package not found: ${packageName}`)

    if (option.saveDev) {
      dependencyMap.devDependencies[packageName] = `^${latestVersion}`
    } else {
      dependencyMap.dependencies[packageName] = `^${latestVersion}`
    }
  }

さっきと同じコマンド実行したら、ちゃんと最新版になった。

package.json
{
  "name": "dev",
  "version": "1.0.0",
  "dependencies": {
    "axios": "^0.27.2"
  },
  "devDependencies": {
    "typescript": "^4.8.2"
  }
}

TODO コメント書いてる通り、バージョン指定インストールも実装する。

shingo.sasakishingo.sasaki
install axios

なら最新版を

install axios@0.26.0

ならそのバージョン制約に従うようにする。

shingo.sasakishingo.sasaki

バリデーションとかまで面倒見ずに、 @ があればそこで区切るだけにしよう。

最新バージョンを取り出す関数を切り出しとく

/**
 * パッケージの最新バージョンを返す
 */
export async function resolvePackageLatestVersion(packageName: string) {
  const latestPackageInfo = await resolvePackage(packageName, '*')
  if (!latestPackageInfo) throw new Error(`Package not found: ${packageName}`)
  return latestPackageInfo.version
}

バージョン制約があればそれを、なければ↑を使うように修正

  // 追加インストールするパッケージを dependencies または devDependencies に追加する
  // バージョン指定がない場合は、最新バージョンを確認してそれを使用する
  for (const packageName of packageNames) {
    const hasConstraint = packageName.includes('@')
    const name = hasConstraint ? packageName.split('@')[0] : packageName
    const constraint = hasConstraint ? packageName.split('@')[1] : `^${await resolvePackageLatestVersion(name)}`

    if (option.saveDev) {
      dependencyMap.devDependencies[name] = constraint
    } else {
      dependencyMap.dependencies[name] = constraint
    }
  }

これで

$ node ../dist/cli.js install axios@0.26.0

ならちゃんと

package.json
{
  "name": "dev",
  "version": "1.0.0",
  "dependencies": {
    "axios": "0.26.0"
  }
}

になるように。

shingo.sasakishingo.sasaki

まだまだガバガバだけど、一旦バージョンコンフリクトの対処以外はここまででヨシとしようかな。

shingo.sasakishingo.sasaki

バージョンコンフリクトが発生する例が一つ欲しいな。
Vite を入れれば何かで発生した気がする。

shingo.sasakishingo.sasaki

webpack 一つ入れるだけで estraverse でバージョンコンフリクト起こってくれた。

  • webpack -> eslint-scope -> estraverse@^4.1.1
  • webpack -> eslint-scope -> esrecurse -> estraverse@^5.2.0

これでさらに直下に estraverse@^3.0.0 なんて追加すれば3バージョンコンフリクトが起こるから動作確認としては良さそう。

shingo.sasakishingo.sasaki
package.json
{
  "name": "dev",
  "version": "1.0.0",
  "dependencies": {
    "webpack": "^5.74.0",
    "estraverse": "^3.0.0"
  }
}

これで npm install すると3バージョンはいる。良い例だ。

$ find node_modules -name estraverse
node_modules/eslint-scope/node_modules/estraverse
node_modules/esrecurse/node_modules/estraverse
node_modules/estraverse

リポジトリ直下の方を ^3.0.0 じゃなくて ^5.0.0 にすればそっちで解決されるようになるし、動作確認の例としては充分そう。

shingo.sasakishingo.sasaki

現状の自作パッケージマネージャでこれをインストールすると1個しか入らない。

$ find node_modules -name estraverse
node_modules/estraverse

version は 3.1.
当然先勝ちになるので直下で指定してるバージョンが優先されちゃう。

shingo.sasakishingo.sasaki

別に webpack いらないな。
eslint-scope 直接で充分だわ。

{
  "name": "dev",
  "version": "1.0.0",
  "dependencies": {
    "eslint-scope": "^5.0.0",
    "estraverse": "^3.0.0"
  }
}
shingo.sasakishingo.sasaki

インストールするパッケージが最小限になったから動作確認しやすくなった。

$ node ../dist/cli.js install
[Resolve by manifest] eslint-scope@^5.0.0 to 5.1.1
[Resolve by manifest] esrecurse@^4.3.0 to 4.3.0
[Resolve by manifest] estraverse@^5.2.0 to 5.3.0
[Resolve by manifest] estraverse@^4.1.1 to 4.3.0
[Resolve by manifest] estraverse@^3.0.0 to 3.1.0
[Installed] eslint-scope@5.1.1
[Installed] estraverse@3.1.0
[Installed] esrecurse@4.3.0
shingo.sasakishingo.sasaki

何気に Lock ファイル見るとちゃんと3バージョン入ってるんだな。

{
  "eslint-scope@^5.0.0": {
    "version": "5.1.1",
    "url": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
    "shasum": "e786e59a66cb92b3f6c1fb0d508aab174848f48c",
    "dependencies": {
      "esrecurse": "^4.3.0",
      "estraverse": "^4.1.1"
    }
  },
  "esrecurse@^4.3.0": {
    "version": "4.3.0",
    "url": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
    "shasum": "7ad7964d679abb28bee72cec63758b1c5d2c9921",
    "dependencies": {
      "estraverse": "^5.2.0"
    }
  },
  "estraverse@^5.2.0": {
    "version": "5.3.0",
    "url": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
    "shasum": "2eea5290702f26ab8fe5370370ff86c965d21123",
    "dependencies": {}
  },
  "estraverse@^4.1.1": {
    "version": "4.3.0",
    "url": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
    "shasum": "398ad3f3c5a24948be7725e83d11a7de28cdbd1d",
    "dependencies": {}
  },
  "estraverse@^3.0.0": {
    "version": "3.1.0",
    "url": "https://registry.npmjs.org/estraverse/-/estraverse-3.1.0.tgz",
    "shasum": "15e28a446b8b82bc700ccc8b96c78af4da0d6cba",
    "dependencies": {}
  }
}

コード的に、解決結果を Lock ファイルに書き出しはしてるけど、インストールリストへの追加時にパッケージ名で上書きしてるからか。

  // 解決結果を Lock ファイルに書き出す
  addLockFile(packageName, vc, packageInfo.version)

  // インストールリストに自身を追加する
  packageList[packageName] = packageInfo.version
shingo.sasakishingo.sasaki

すんごい実装になってしまった気がする。

/**
 * パッケージのバージョン解決を再帰的に行い、依存パッケージの依存パッケージまで深さ優先で解決していく
 * @argument topLevelList node_modules 直下にインストール可能なパッケージリスト
 * @argument conflictedList バージョン衝突のため、直下でなく各パッケージ以下の node_modules にインストールするパッケージリスト
 * @argument dependencyStack 現在解決中のパッケージに依存するパッケージ名のスタック
 */
export async function collectDepsPackageList(
  name: PackageName,
  vc: VersionConstraint,
  rootDependenciesMap: Readonly<DependenciesMap>,
  topLevelList: DependenciesMap,
  conflictedList: ConflictedPackageInfo[],
  dependencyStack: PackageName[]
) {
  // 自身のパッケージ名を依存スタックに積み上げる
  dependencyStack.push(name)

  // 解決後のパッケージ情報を取得する
  const packageInfo = await resolvePackage(name, vc)

  // 解決結果を Lock ファイルに書き出す
  addLockFile(name, vc, packageInfo.version)

  // どこにインストールするかのヒント
  const topLevelExists = !!topLevelList[name]
  const isCompatibleToTopLevel = topLevelExists && semver.satisfies(topLevelList[name], vc)
  const isRootDependency = dependencyStack.length === 1

  // 1. 初出パッケージで、自身がルートからの依存であればトップレベルに追加
  if (!topLevelExists && isRootDependency) {
    topLevelList[name] = packageInfo.version
  }
  // 2. 初出パッケージで、ルートからの依存が存在しなければ自身をトップレベルに追加
  else if (!topLevelExists && !rootDependenciesMap[name]) {
    topLevelList[name] = packageInfo.version
  }
  // 3. 初出パッケージで、ルートからの依存が存在するが
  else if (!topLevelExists && rootDependenciesMap[name]) {
    const rootDependencyVersion = (await resolvePackage(name, rootDependenciesMap[name])).version
    // 3-1. 自身と互換性のあるバージョンであればスルー
    if (semver.satisfies(rootDependencyVersion, vc)) {
      // 何もしない
    }
    // 3-2. 自身と互換性がないバージョンであれば、衝突リストに追加
    else {
      conflictLog(name, vc, rootDependenciesMap[name])
      conflictedList.push({ name, version: packageInfo.version, parent: dependencyStack[dependencyStack.length - 2] })
    }
  }
  // 4. 既出パッケージで、自身と互換性のあるバージョンであればスルー
  else if (topLevelExists && isCompatibleToTopLevel) {
    // 何もしない
  }
  // 5. 既出パッケージで、自身と互換性がないバージョンであれば、衝突リストに追加
  else {
    conflictLog(name, vc, rootDependenciesMap[name])
    conflictedList.push({ name, version: packageInfo.version, parent: dependencyStack[dependencyStack.length - 2] })
  }

  // 自身が依存するパッケージに対して再帰的に同様の操作を行う
  for (const [depName, depVersion] of Object.entries(packageInfo.dependencies)) {
    await collectDepsPackageList(
      depName,
      depVersion,
      rootDependenciesMap,
      topLevelList,
      conflictedList,
      dependencyStack
    )
  }

  // 自身の解決は全て完了したのでスタックから取り除く
  dependencyStack.pop()
}

でもちゃんと動く。

$ node ../dist/cli.js install
[Resolve by manifest] eslint-scope@^5.0.0 to 5.1.1
[Resolve by manifest] esrecurse@^4.3.0 to 4.3.0
[Resolve by manifest] estraverse@^5.2.0 to 5.3.0
[Resolve by manifest] estraverse@^3.0.0 to 3.1.0
[Conflict with root] estraverse@^5.2.0 is conflicted with ^3.0.0
[Resolve by manifest] estraverse@^4.1.1 to 4.3.0
[Resolve by manifest] estraverse@^3.0.0 to 3.1.0
[Conflict with root] estraverse@^4.1.1 is conflicted with ^3.0.0
[Resolve by manifest] estraverse@^3.0.0 to 3.1.0
[Installed] eslint-scope@5.1.1 > node_modules/eslint-scope
[Installed] esrecurse@4.3.0 > node_modules/esrecurse
[Installed] estraverse@3.1.0 > node_modules/estraverse
[Installed] estraverse@5.3.0 > node_modules/esrecurse/node_modules/estraverse
[Installed] estraverse@4.3.0 > node_modules/eslint-scope/node_modules/estraverse
$ find node_modules -name estraverse
node_modules/eslint-scope/node_modules/estraverse
node_modules/esrecurse/node_modules/estraverse
node_modules/estraverse
shingo.sasakishingo.sasaki

これまでの Vite や axios のパターンも再度動作確認した限りは大丈夫そう。
これで一旦終わりかなぁ。リファクタしていこう。

shingo.sasakishingo.sasaki

npm 経由でインストールする

$ sudo npm install --global sasaki-package-manager

実行ファイルができてる

$ which sasaki-pm
/usr/local/bin/sasaki-pm

できてるけど、マジックコメントが入ってないぞ。

shingo.sasakishingo.sasaki

元パッケージの方でビルド試してみたけど、別にビルドで勝手に挿入されるってわけじゃないのか。
別にプロセスが必要っぽいけど今回は dist/cli.js に手動で付けてみるか。

shingo.sasakishingo.sasaki

動きそう

$ sasaki-pm
Usage: sasaki-pm [options] [command]

Options:
  --production               install only dependencies (not devDependencies)
  --save-dev                 Package will appear in your devDependencies
  -h, --help                 display help for command

Commands:
  install [packageNames...]
  help [command]             display help for command

完璧!

$ sasaki-pm install axios
[Resolve by manifest] axios@* to 0.27.2
[Resolve by manifest] axios@^0.27.2 to 0.27.2
[Resolve by manifest] follow-redirects@^1.14.9 to 1.15.1
[Resolve by manifest] form-data@^4.0.0 to 4.0.0
[Resolve by manifest] asynckit@^0.4.0 to 0.4.0
[Resolve by manifest] combined-stream@^1.0.8 to 1.0.8
[Resolve by manifest] delayed-stream@~1.0.0 to 1.0.0
[Resolve by manifest] mime-types@^2.1.12 to 2.1.35
[Resolve by manifest] mime-db@1.52.0 to 1.52.0
[Installed] axios@0.27.2 > node_modules/axios
[Installed] follow-redirects@1.15.1 > node_modules/follow-redirects
[Installed] form-data@4.0.0 > node_modules/form-data
[Installed] asynckit@0.4.0 > node_modules/asynckit
[Installed] combined-stream@1.0.8 > node_modules/combined-stream
[Installed] delayed-stream@1.0.0 > node_modules/delayed-stream
[Installed] mime-types@2.1.35 > node_modules/mime-types
[Installed] mime-db@1.52.0 > node_modules/mime-db
$ cat sasaki-pm.lock.json 
{
  "asynckit@^0.4.0": {
    "version": "0.4.0",
    "url": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
    "shasum": "c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79",
    "dependencies": {}
  },
  "axios@^0.27.2": {
    "version": "0.27.2",
    "url": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
    "shasum": "207658cc8621606e586c85db4b41a750e756d972",
    "dependencies": {
      "follow-redirects": "^1.14.9",
      "form-data": "^4.0.0"
    }
  },
  "combined-stream@^1.0.8": {
    "version": "1.0.8",
    "url": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
    "shasum": "c3d45a8b34fd730631a110a8a2520682b31d5a7f",
    "dependencies": {
      "delayed-stream": "~1.0.0"
    }
  },
  "delayed-stream@~1.0.0": {
    "version": "1.0.0",
    "url": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
    "shasum": "df3ae199acadfb7d440aaae0b29e2272b24ec619",
    "dependencies": {}
  },
  "follow-redirects@^1.14.9": {
    "version": "1.15.1",
    "url": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
    "shasum": "0ca6a452306c9b276e4d3127483e29575e207ad5",
    "dependencies": {}
  },
  "form-data@^4.0.0": {
    "version": "4.0.0",
    "url": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
    "shasum": "93919daeaf361ee529584b9b31664dc12c9fa452",
    "dependencies": {
      "asynckit": "^0.4.0",
      "combined-stream": "^1.0.8",
      "mime-types": "^2.1.12"
    }
  },
  "mime-db@1.52.0": {
    "version": "1.52.0",
    "url": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
    "shasum": "bbabcdc02859f4987301c856e3387ce5ec43bf70",
    "dependencies": {}
  },
  "mime-types@^2.1.12": {
    "version": "2.1.35",
    "url": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
    "shasum": "381a871b62a734450660ae3deee44813f70d959a",
    "dependencies": {
      "mime-db": "1.52.0"
    }
  }
}
このスクラップは2022/09/05にクローズされました