TL;DR
-
node-sassのような「壊れがちな古い依存」をNode.js 22でnpm installしたら、node-gyp周りのビルドエラーで一瞬でクラッシュした。 python2が見つからず→python3にフォールバック→構文エラー、という初心者にはかなり辛い連鎖 -
同じ依存を
deno installすると、エラーにならずインストール自体は完走した。 Denoはnpmのpostinstall等のライフサイクルスクリプトをデフォルトで実行しないため、壊れたネイティブビルドがそもそも走らない - ただし魔法ではない。実際にそのパッケージを使おうとした瞬間にはDeno側でもちゃんとエラーになる。 ただしエラーメッセージはビルドツールチェーンの迷路ではなく、パッケージ自身が出す分かりやすい一文だった
- 調べる過程で、Denoでは
package.json自体が要らないケースもあると分かった。npm:指定子で直接importするだけで動き、依存はプロジェクトの外の共有キャッシュに1回だけ保存される - 一方でDenoの権限モデル(env/fs/networkのデフォルト拒否)は、実運用ではフラグ管理が細かすぎて
-Aに逃げたくなる、という声も定番だった
やろうとしたこと
npmを使っていて一番こたえるのは、npm installで出る大量のエラーや警告です。特にしばらく触っていなかった古いプロジェクトを久しぶりに開いたとき、以前は普通に動いていたはずなのにnpm installしただけで画面がエラーで真っ赤になる、という経験を何度もしています。Node.jsのバージョンを上げただけでネイティブモジュールのビルドが通らなくなっていたり、聞いたこともないツール(node-gypとか)がエラーを吐いてきたり——自分のコードは1行も変えていないのに、です。「これは自分のせいなのか、パッケージ側が古いせいなのか」を切り分けるだけでもひと苦労で、初心者にはかなり辛いポイントだと思っています。
そんな中で目に入ったのがDeno 2.9(2026年6月25日リリース)の情報です。npm互換を大きく進めた、という触れ込みなので、壊れやすい古い依存を実際に使って、npmとDenoでインストール体験がどう変わるのかを試してみることにしました。調べる過程で、そもそもpackage.jsonすら要らないケースがあることも分かったので、それも合わせて書きます。
構成/環境
- Node.js: v22.22.2 / npm 10.9.7
-
Deno: 2.9.5(
npm install -g denoで導入。公式は@deno/linux-x64-glibc等のプラットフォームバイナリをnpmのoptionalDependenciesとして配布しており、npmレジストリ経由でインストールできた) - 検証用に
chalk(色付け)とnanoid(ID生成)に依存する小さなプロジェクトを作成
$ deno --version
deno 2.9.5 (stable, release, x86_64-unknown-linux-gnu)
v8 15.0.245.2-rusty
typescript 6.0.3
やってみた結果
まずは普通のnpmプロジェクト(比較の基準)
{
"name": "deno-vs-node-demo",
"type": "module",
"dependencies": { "nanoid": "^5.0.7", "chalk": "^5.3.0" }
}
npm installするとpackage-lock.jsonが生成され、node_modulesができます。ここまではいつも通りです。
import { nanoid } from "nanoid";
import chalk from "chalk";
console.log(chalk.green(`ID: ${nanoid()}`));
$ node index.js
ID: 8NsWQ715S8IIQOwMbSayT
古い依存で試す — npm installが本当に「壊れる」瞬間
chalkやnanoidのようなメンテされているパッケージだけなら、npmも特に問題は起きません。今回確かめたかったのは、「昔は動いていたのに、今動かすとエラーになる」古い依存を使った場合です。題材に選んだのは、ネイティブビルドが必要なCSSプリプロセッサnode-sass(2020年頃から非推奨、後継はsass)です。
{ "dependencies": { "node-sass": "^4.14.1" } }
$ npm install
npm error gyp verb check python checking for Python executable "python2" in the PATH
npm error gyp verb `which` failed Error: not found: python2
npm error gyp verb check python checking for Python executable "python" in the PATH
npm error gyp verb `which` succeeded python /usr/local/bin/python
npm error gyp ERR! configure error
npm error gyp ERR! stack Error: Command failed: /usr/local/bin/python -c import sys; print "%s.%s.%s" % sys.version_info[:3];
npm error gyp ERR! stack File "<string>", line 1
npm error gyp ERR! stack import sys; print "%s.%s.%s" % sys.version_info[:3];
npm error gyp ERR! stack ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
npm error gyp ERR! stack SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
npm error gyp ERR! node -v v22.22.2
npm error gyp ERR! node-gyp -v v3.8.0
npm error gyp ERR! not ok
npm error Build failed with error code: 1
これがまさに「あるある」だと思います。node-sassはインストール時にネイティブバイナリをその場でビルドする仕組みで、ビルドツールnode-gypが「Python 2」を探しにいきます。今どきの環境にPython 2は入っていないので、代わりに見つかったPython 3にフォールバックするのですが、当時(Python 2時代)の書き方で書かれたビルドスクリプトのprint "..."という構文が、Python 3では文法エラーになります。自分のコードは1行も書いていないのに、環境が新しくなっただけで積み木のように崩れる、というのがこの手のエラーの正体でした。
同じ依存をDenoで試す — インストールは通る、でも問題自体は消えない
同じpackage.jsonのまま、deno installを実行してみました。
$ deno install
Download https://registry.npmjs.org/node-sass
Installed 187 packages in 223ms
Dependencies:
+ npm:node-sass 4.14.1
╭ Warning
│ The following packages are deprecated:
│ npm:request@2.88.2 (request has been deprecated, see https://github.com/request/request/issues/3142)
│ npm:tar@2.2.2 (Old versions of tar are not supported, and contain widely publicized security vulnerabilities...)
│ npm:node-sass@4.14.1 (Node Sass is no longer supported. Please use `sass` or `sass-embedded` instead.)
│ (他10件省略)
╰─
クラッシュせず、最後まで完走しました。 node-gypが呼ばれた形跡すらありません。理由はdeno install --helpに書いてありました。
--allow-scripts[=<PACKAGE>...] Allow running npm lifecycle scripts for the given packages
Note: Scripts will only be executed when using a node_modules directory (--node-modules-dir)
Denoはnpmのpostinstall等のライフサイクルスクリプトを、デフォルトでは一切実行しません。 node-sassのネイティブビルド(=さっき壊れた張本人)もその1つなので、インストール時点では何も起きなかった、というわけです。その代わり、非推奨パッケージの一覧は上のように見やすい箱にまとめて警告してくれていて、npmのインストールログの中に埋もれがちな警告よりも一覧性は高いと感じました。
ただし、これでめでたしめでたし、ではありません。実際にこの依存を使おうとすると——
import sass from "node-sass";
console.log(sass.renderSync({ data: "body{color:red}" }).css.toString());
$ deno run --allow-read --allow-env check.js
error: Uncaught (in promise) Error: Node Sass does not yet support your current environment: Linux 64-bit with Unsupported runtime (147)
For more information on which environments are supported please see:
https://github.com/sass/node-sass/releases/tag/v4.14.1
やっぱりエラーになります。 node-sass自体がビルド済みの実行環境を要求するパッケージなので、ビルドをスキップしただけでは動くようにはなりません。ここは正直、Denoだから解決する問題ではありませんでした。
ただ、エラーの質は大きく違いました。 npm側はnode-gyp→Python→構文エラーという、自分のコードから3階層も4階層も潜った場所で止まり、原因を特定するだけでもひと苦労です。一方Deno側は、node-sass自身のコードが「このバイナリ、今の環境では動かないよ」とはっきり教えてくれるところで止まります。インストールの時点で全体が止まるか、実際に使う時点で局所的に止まるかの違いは、地味に効いてくる差だと感じました。
package.jsonなしでnpmパッケージを使ってみる — 本当に要らなかった
ここまでの検証で気になったのが、そもそもpackage.json自体はどこまで必要なのか、という点です。空のディレクトリにpackage.jsonを一切作らず、.tsファイル1つだけを置いてみました。importの書き方だけ、パッケージ名の前にnpm:と付けます。
// main.ts (package.jsonなし、node_modulesなし)
import { nanoid } from "npm:nanoid@5.0.7";
import chalk from "npm:chalk@5.3.0";
console.log(chalk.green(`ID: ${nanoid()}`));
$ deno run --allow-env main.ts
Download https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz
Download https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz
ID: OKUtaA6IkC6PN3fIBw92u
普通に動きました。しかも実行後にディレクトリを覗いても、増えたファイルは1つもありません。
$ ls -la
main.ts
node_modulesはもちろん、package-lock.jsonもdeno.lockもできていません。deno infoで依存関係を確認すると、main.tsから直接npm:nanoidとnpm:chalkにぶら下がっているのが分かります。
$ deno info main.ts
main.ts (126B)
├── npm:/nanoid@5.0.7 (10.69KB)
└── npm:/chalk@5.3.0 (42.71KB)
ダウンロードした中身はどこに行ったのかというと、プロジェクトの外、ホームディレクトリ配下のグローバルキャッシュ(~/.cache/deno) です。試しに全く別のディレクトリで同じnpm:chalk@5.3.0をimportするコードを書いて実行すると、ダウンロードのログすら出ずに一瞬で動きました。
$ cd /tmp/別のプロジェクト
$ deno run --allow-env m.ts
別プロジェクトから同じ依存を使う
エラーの発生ポイントに出てくるパスを見ても、file:///root/.cache/deno/npm/registry.npmjs.org/chalk/5.3.0/...と、プロジェクトの外にある共有キャッシュを直接参照しているのが分かります。npmだと「プロジェクトごとにnode_modules/chalkが重複して存在する」のが普通ですが、Denoはバージョンごとに1回だけ保存して、複数プロジェクトで使い回すという設計です(CargoやGoのモジュールキャッシュに近い発想です)。
npmパッケージを使うためだけなら、package.jsonもnode_modulesも本当に要りませんでした。 依存の数が増えるほどpackage.json/package-lock.json/node_modulesという3点セットの管理コストが積み上がっていくnpmとは、そもそもの前提から違います。
deno.jsonを足すとどう変わるか — それでもpackage.jsonではない
npm:nanoid@5.0.7のようにバージョンまで毎回書くのは少し冗長です。エイリアスを付けたくなったらdeno.jsonを追加します。
{
"imports": {
"chalk": "npm:chalk@5.3.0",
"nanoid": "npm:nanoid@5.0.7"
}
}
// main2.ts — 見慣れた書き方に近づく
import { nanoid } from "nanoid";
import chalk from "chalk";
console.log(chalk.blue(`ID(via import map): ${nanoid()}`));
$ deno run --allow-env main2.ts
ID(via import map): 8o4s3tVFGl9WjGxo8DMUJ
ここで初めてdeno.lockが生成されました。deno.jsonを置いた時点でDenoは「このディレクトリはプロジェクトだ」と認識し、依存を固定するためのロックファイルを作るようです。ただし**node_modulesは相変わらず作られません。** deno.jsonはimportのエイリアスやdeno taskのスクリプト定義程度の軽い役割で、npmのpackage.jsonのように「node_modulesを組み立てるための実行装置」ではない、というのが実際に触ってみた感覚です。
既存npmプロジェクトを移行するなら — deno installでpackage-lock.jsonを読ませる
とはいえ、世の中の資産は当然もうpackage.jsonベースで書かれています。「今あるnpmプロジェクトはどうなるのか」 も試しました。node_modulesを消し、package.jsonとpackage-lock.jsonだけが残った状態でdeno installを実行します。
$ deno install
Seeded deno.lock from .../package-lock.json
Download https://registry.npmjs.org/nanoid
Download https://registry.npmjs.org/chalk
Initialize nanoid@5.1.16
Initialize chalk@5.6.2
Dependencies:
+ npm:chalk 5.6.2
+ npm:nanoid 5.1.16
生成されたdeno.lockの中身をpackage-lock.jsonと突き合わせたところ、バージョン(chalk 5.6.2 / nanoid 5.1.16)もintegrityハッシュも完全に一致していました。「移行はコマンド一発」という触れ込みは、少なくともこの規模のプロジェクトでは誇張ではなかったです。
{
"npm": {
"chalk@5.6.2": { "integrity": "sha512-7NzBL0..." },
"nanoid@5.1.16": { "integrity": "sha512-kVrnsr...", "bin": true }
}
}
ただし、ここでは先ほどと違って**node_modulesもちゃんと生成されます。** package.jsonが存在するプロジェクトは「npm互換モード」として扱われ、Node.js向けツールなどがnode_modulesを前提に動くケースに配慮しているようです。整理すると、こういう住み分けでした。
| package.jsonなし(Denoネイティブ) | package.jsonあり(npm互換モード) | |
|---|---|---|
| 依存の書き方 |
npm:chalk@5.3.0を直接import、またはdeno.jsonのimport map |
通常のnpmと同じimport chalk from "chalk"
|
| 依存の保存先 | グローバルキャッシュ(~/.cache/deno)のみ |
プロジェクト内node_modules(npmと同じ) |
| ロックファイル |
deno.jsonを置くまでは無し |
deno installでdeno.lock(package-lock.jsonから移行可) |
権限モデルの違い — 「自分のコード」だけの話じゃなかった
まず前提から。Node.jsではnpm installした瞬間、そのパッケージ(そしてそのパッケージがさらに読み込んでいる別のパッケージも)は、ファイルの読み書き・ネットワーク通信・環境変数の参照が全部フリーパスになります。自分の書いたコードも、依存先のコードも、区別なく同じ権限で動きます。
Denoはこの前提が違っていて、「自分のコード」と「依存パッケージのコード」を区別せず、プロセス全体としてファイル/ネットワーク/環境変数へのアクセスをデフォルトで全部拒否します。使いたい機能があれば、--allow-envのように必要な権限だけを起動時に渡します。
言葉で説明されてもピンと来なかったので、実際に試しました。使ったのはchalk(ターミナルの文字に色を付けるだけの定番ライブラリ)です。自分のコードではchalk.green(...)を呼んでいるだけで、process.envには一度も触れていません。それでも権限フラグなしで実行すると、エラーになりました。
$ deno run index.js
error: Uncaught (in promise) NotCapable: Requires env access to "TF_BUILD", run again with the --allow-env flag
at ... node_modules/.deno/chalk@5.6.2/node_modules/chalk/source/vendor/supports-color/index.js:86:17
エラーメッセージのファイルパスを辿ると、chalkが内部で使っているsupports-colorというモジュールが原因でした。「今CI環境で動いているか」を判定するために、process.env.TF_BUILD(CIサービスが自動で立てる環境変数の1つ)を読もうとしていたのです。整理するとこうなります。
- 自分は
process.envを一度も書いていない - でも自分が
importしたchalkが、内部でさらにsupports-colorを呼んでいる - その
supports-colorが環境変数を読もうとした
3段階も奥にあるアクセスでも、Denoの権限チェックはちゃんと反応します。 「自分のコード」「依存パッケージ」「そのまた依存パッケージ」という区別なく、プロセス全体が1つの権限の範囲の中で動いている、というのがDenoの考え方です。--allow-envを付ければ、この一連のアクセスがまとめて許可されます。
$ deno run --allow-env index.js
ID: UCaJxxbgeCPvKVbxtYTLO
ENV HOME = /root
ファイル書き込みも同様に試すと、権限なしではNotCapableエラー、--allow-writeを付けて初めて書き込めました。
| Node.js | Deno | |
|---|---|---|
| インストール直後の依存パッケージの権限 | 無制限(env/fs/networkすべて可) | デフォルトで全拒否 |
| 権限の付与単位 | なし(all-or-nothing) |
--allow-env/--allow-write/--allow-net等で個別指定 |
| 依存の依存が勝手に何かする場合 | 気づきにくい | 実行時にエラーで気づける |
Node.jsでは意識したことすらなかった発想ですが、Denoでは依存パッケージ(とその奥のパッケージ)が何にアクセスしようとしているかが、実行時のエラーという形で強制的に見えてしまいます。 package.json不要という身軽さの裏に、こういう仕組みが入っているのは覚えておいて損はなさそうです。
ただ、ここまで持ち上げると一面的なので、実際に使っている人たちの声も探してみました。今回はchalkとnanoidという依存2つだけの小さな例でしたが、実務のプロジェクトでnpm由来の依存を何十個も抱えると、--allow-envだけでなく--allow-read・--allow-write・--allow-net……とフラグがどんどん積み上がっていくのは容易に想像がつきます。実際、海外・日本語どちらの記事でもこの点への不満は定番でした。
- Deno公式ドキュメント自身が、「
--allow-all(-A)はセキュリティサンドボックスを完全に無効化し、Node.jsでスクリプトを実行するのと同じ(=ゼロの)セキュリティ特性になる」と明記した上で、それでも多くの開発者が結局-Aに流れてしまう傾向があることを認めています(Deno公式Permissionsドキュメント) - 実際のGitHub Issueでは、「権限エラーは出るが、どのファイルへのアクセスを求めているのか分からず、範囲を絞れないまま
--allow-readを丸ごと渡すしかなかった」という具体的な不満も報告されています(denoland/deno#19864) - Denoのコアコントリビューター自身が書いた記事でも、「付与した権限はモジュール単位ではなくプログラム全体に一括で効く」という粒度の粗さが指摘されています(Deno の Permission の話 - Qiita)
使えないわけではないけど、細かすぎて辛い。
というのが、実際に使っている人たちの本音に近そうです。今回みたいな小さい検証コードなら「気づかせてくれる良い仕組み」で終わりますが、依存が増えるほど「結局-A」に流れる誘惑は強くなる、というのは正直なところとして書いておきます。
TypeScriptがそのまま動く、でも本当に「そのまま」なのはDenoだけだった
ここからは少し話題が変わって、TypeScriptの実行方法についてです。
普段npmでTypeScriptを使うときは、ts-nodeやtsxのようなツールを追加で入れるか、tscで一度JavaScriptにビルドしてから実行する、というひと手間が必要でした。Denoの分かりやすい強みの1つが「その手間なしでTypeScriptファイルをそのまま実行できる」という点です。
ただ最近は「Node.jsもTypeScriptをそのまま実行できるようになった」というニュースも見かけるようになっていて、じゃあ結局DenoとNode.jsで何が違うのか気になりました。そこで同じTypeScriptファイルを両方で実行して、挙動を比べてみることにしました。
まずは型注釈だけを使ったシンプルな例です。
interface Greeting { name: string; }
const g: Greeting = { name: "Qiita" };
console.log(`Hello, ${g.name}!`);
$ deno run hello.ts
Hello, Qiita! (TypeScript直接実行)
$ node hello.ts
Hello, Qiita! (TypeScript直接実行)
あれ、Node.js 22でもそのまま動きました。「型注釈を付けているだけ」ならDenoと差はなさそうです。ここで終わっていたら「もうNode.jsで十分では」という結論になっていたと思います。ただ、もう少しTypeScriptらしい書き方——enum(列挙型)を使うとどうなるか試してみました。
enum Status { Active, Inactive }
console.log(Status.Active);
$ deno run enum-test.ts
0
$ node enum-test.ts
file:///.../enum-test.ts:1
> enum Status {
node:internal/modules/run_main:123
今度ははっきり差が出ました。Denoは動くのに、Node.jsはエラーで落ちます。
なぜこうなるかというと、両者がやっていることの中身が違うからです。
-
Node.js 22の型サポートは「型ストリッピング」 という仕組みで、
interfaceや: numberのような型注釈の文字をただ消すだけです。interface Greeting { name: string; }の行は実行時には丸ごと不要なので、消してしまえばそれで済みます - 一方
enum Status { Active, Inactive }は、「型注釈」ではなく実行すると実際に値を持つオブジェクトを生成する構文です。ただ文字を消すだけでは対応できず、JavaScriptとして動くコードに変換する必要があります -
Denoは
tsc(TypeScriptの公式コンパイラ)相当のフル変換をしているので、enumのような「変換が要る構文」もちゃんと処理してから実行します
つまり同じ「TypeScriptに対応した」でも、Node.jsは「型注釈を消すだけ」、Denoは「TypeScriptとしてちゃんとコンパイルする」で、対応の深さが違います。「Node.jsもTS対応した」というニュースの見出しだけを見て同じものだと思っていたのは、自分の誤解でした。
標準ツールとおおよその起動速度
deno fmt・deno lintは追加インストールなしでそのまま動きました。Prettier/ESLintの設定ファイルを書く手間がないのは素直に楽です。
$ deno lint lint-test.ts
error[no-unused-vars]: `unused` is never used
Found 1 problem
起動速度も5回実行の合計で比べてみましたが、この検証環境ではNode.jsとDenoでほぼ差がありませんでした(Node.js 0.333秒 / Deno 0.293秒、5回合計)。Deno公式が謳う「コールドスタート17ms」はデスクトップアプリ(deno desktop)のバイナリ起動時の数値で、今回のようなCLIスクリプト実行とは条件が違うので、単純比較はできません。ここは誇張せず、体感差はなかったと正直に書いておきます。
学んだこと
- ✅ 「古い依存でnpm installが壊れる」は、node-gyp経由のネイティブビルドが原因になっているケースが多い。 Denoはpostinstall等のライフサイクルスクリプトをデフォルトで実行しないので、この手のクラッシュはインストール時点では起きない
- ⚠️ ただし壊れているパッケージ自体が直るわけではない。問題が「インストール全体を止める」から「実際に使った瞬間にだけ止まる」に変わる、というのが正確なところ。 それでもエラーメッセージの分かりやすさは段違いだった
- ✅
package.jsonは本当に要らなかった。npm:指定子で直接importするだけでnpmパッケージが動き、依存はプロジェクトの外のグローバルキャッシュに1回だけ保存されて使い回される - ✅ 既存のnpmプロジェクトを持っている場合の移行パスも用意されている。
package.jsonを残せばdeno installでpackage-lock.jsonから一発移行でき、この場合はnode_modulesも生成される(=npm互換モードとネイティブモードは明確に別物) - ⚠️ 権限モデルは依存の奥のコードにも効く便利な仕組みだが、依存が増えるほどフラグ管理が細かすぎて辛くなる、という声が海外・日本語問わず定番だった
おわりに
「古いプロジェクトを開くたびにnpm installがエラーだらけになる」というのが今回の出発点でしたが、Denoに乗り換えれば全部解決、という単純な話ではありませんでした。壊れたパッケージが直るわけではないし、権限モデルも運用が楽なわけではない。それでも、インストール全体を巻き込んで止まらないことと、そもそもpackage.jsonという管理対象を持たずに済む選択肢があることの2つは、npmの「重荷」を減らす方向として素直に手応えがありました。
次はDeno 2.9のもう一つの目玉、deno desktop(Webプロジェクトをそのままネイティブアプリ化する機能)を実際に試してみる予定です。package.json不要のパッケージ管理や権限モデルで気になった点があれば、ぜひコメントで教えてください。
追記(2026-08-12): npm v12 では前提が変わっていました
コメントで 「現在の npm (v12以降) は preinstall / install / postinstall をデフォルト実行しないのでは」 というご指摘をいただきました。確認したところ完全にその通りで、この記事の「npm install が壊れる」という前提は npm 10 系での挙動 でした。書き換えると論旨が追えなくなるので、本文はそのままに、ここで補足します。
何が変わったか
npm v12 は 2026年7月8日にリリースされ(6月9日に GitHub Changelog で事前アナウンス)、インストール時のデフォルトが3つ+1つ変わりました。
| 変更 | 内容 |
|---|---|
| ライフサイクルスクリプト | 依存パッケージの preinstall / install / postinstall はデフォルト実行されない。allowScripts に書いたパッケージだけ実行される |
| 暗黙の node-gyp リビルド |
binding.gyp を含むパッケージの自動リビルドもブロック対象に。従来これは --ignore-scripts でも防げない経路だった |
--allow-git |
デフォルト none。Git 依存(直接・推移とも)が解決されなくなる |
--allow-remote |
デフォルト none。https tarball など URL 指定の依存が解決されなくなる |
本記事の検証がどうなるか
今回 node-sass が壊れたのは、install/postinstall スクリプトから node-gyp が走る経路です。v12 ではスクリプト自体も、binding.gyp による暗黙のリビルドも止まるので、おそらく npm install はクラッシュせずに完走します。
つまり本記事の比較は、正確には
- ❌ 「npm は壊れる / Deno は壊れない」
- ⭕ 「インストール時にライフサイクルスクリプトを実行する設計か、しない設計か」
の差でした。そして後者はもう Deno 固有ではありません。pnpm は v10 で、Bun はそれ以前からライフサイクルスクリプトをデフォルト無効にしており、npm 12 でそこに合流した、という位置づけになります。Deno の --allow-scripts=<PACKAGE> と npm 12 の allowScripts は、発想としてほぼ同じものです。
なお、v12 で allowScripts に node-sass を明示的に許可すれば、当然ながら本文と同じ node-gyp のエラーに戻ります。壊れたパッケージが直るわけではないのは、Deno のときと同じです。
記事のどこが残るか
| 本文の主張 | npm v12 を踏まえて |
|---|---|
npm install が node-gyp 経由でクラッシュする |
⚠️ npm 10 系での話。v12 では起きない見込み |
| Deno はスクリプトを実行しないので完走する | ⚠️ Deno 固有の強みではない(pnpm 10 / Bun / npm 12 も同様) |
| 壊れた依存は「実際に使った瞬間」にエラーになる | ✅ そのまま有効。npm 12 でも同じ形になる |
package.json なしで npm: 指定子だけで動く |
✅ 影響なし |
| 権限モデル(env/fs/net のデフォルト拒否) | ✅ 影響なし。npm 12 はインストール時の話で、実行時のサンドボックスではない |
| Node.js の型ストリッピング vs Deno のフルコンパイル | ✅ 影響なし |
手元で試すなら
検証環境は Node.js 22 同梱の npm 10.9.7 のままでした。手元の npm を確認・更新するには:
npm -v # Node.js 22 同梱は 10.9.x
npm install -g npm@12
いきなり上げるのが不安な場合、npm 11.16.0 以降なら v12 の変更に関する警告が出るので、先に影響範囲を確認できます。
npm install # 未承認のスクリプトがあれば警告が出る
npm approve-scripts --allow-scripts-pending # 未承認のままブロックされるパッケージを一覧表示(読み取り専用)
npm approve-scripts # 対話的に承認して package.json の allowScripts に記録
承認結果は package.json の allowScripts に記録されるので、コミットしておけば v12 に上げても必要なスクリプトだけが動きます。bcrypt / sqlite3 / sharp のようにネイティブビルドを正当な理由で使っているパッケージがある場合は、この作業が必要になります。
ご指摘ありがとうございました。
参考

