@s2j/global-npm 2.0.3 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # 📦 Global npm Package Setup
2
2
 
3
3
  Node.js の **グローバル npm パッケージ管理** を `package.json` で一元化する CLI です。
4
- macOS / Windows 11で同じ `global-npm check|update|install` フローを使えます。
4
+ macOS / Windows 11で同じ `global-npm` フローを使えます。
5
5
 
6
6
  GitHub: [stein2nd/global-npm-setup](https://github.com/stein2nd/global-npm-setup)
7
7
 
@@ -11,9 +11,11 @@ GitHub: [stein2nd/global-npm-setup](https://github.com/stein2nd/global-npm-setup
11
11
  global-npm check # グローバルパッケージの更新確認 (ncu)
12
12
  global-npm update # package.json のバージョン範囲を更新 (ncu -u)
13
13
  global-npm install # dependencies を列挙して npm install -g を実行
14
+ global-npm sync # upstream + user-deps → 実効 package.json を再生成
15
+ global-npm add # user-deps.json にパッケージを追記
14
16
  ```
15
17
 
16
- 定番フロー:
18
+ 定番フローは、下記の順番になるかと思います。なお、`install` 単体では ncu は実行しません。
17
19
 
18
20
  ```sh
19
21
  global-npm check
@@ -21,7 +23,22 @@ global-npm update
21
23
  global-npm install
22
24
  ```
23
25
 
24
- `install` 単体では ncu は実行しません。
26
+ ## しくみ (v2.1)
27
+
28
+ | レイヤ | 場所 | 役割 |
29
+ |--------|------|------|
30
+ | Upstream 正本 | `@s2j/global-npm` 同梱 `package.json` | 公式 `dependencies` 一覧 |
31
+ | ユーザー overlay | `$SETUP_DIR/user-deps.json` | 追加分、ピン留め |
32
+ | 実効 package.json | `$SETUP_DIR/package.json` | ncu / install が読む実効マニフェスト |
33
+
34
+ **setup ディレクトリ (`$SETUP_DIR`) のデフォルト**
35
+
36
+ | OS | パス |
37
+ |----|------|
38
+ | macOS / Linux | `~/.config/global-npm` |
39
+ | Windows 11 | `%APPDATA%\global-npm` |
40
+
41
+ `GLOBAL_NPM_SETUP_DIR` 環境変数で上書きできます。詳細は [docs/layout.md](./docs/layout.md) をご覧ください。
25
42
 
26
43
  ## セットアップ
27
44
 
@@ -36,8 +53,9 @@ Homebrew、Node.js v18以降が未導入の場合は、先にインストール
36
53
  2. `node --version`、`npm --version` でバージョンを確認する。
37
54
  3. 移行する場合、`~/bin/global-npm` (Zsh ラッパー) が PATH に残っていないか確認する。残っている場合は削除する (npm グローバル bin の `global-npm` と競合する場合がある)。
38
55
 
39
- 推奨配置: `~/dotfiles/global-npm-setup/`
40
- npm グローバル bin: 通常 `$(npm prefix -g)/bin` (nvm 利用時はその Node に紐づく prefix)
56
+ * 推奨配置 (開発): `~/dotfiles/global-npm-setup/`
57
+ * setup ディレクトリ: `~/.config/global-npm`
58
+ * npm グローバル bin: 通常 `$(npm prefix -g)/bin` (nvm 利用時はその Node に紐づく prefix)
41
59
 
42
60
  ### Windows での下準備
43
61
 
@@ -52,36 +70,54 @@ Node.js v18以上 (LTS 推奨) が未導入の場合は、先にインストー
52
70
  3. `node --version`、`npm --version` でバージョンを確認する。
53
71
  4. PowerShell を再起動後、`global-npm check` (導入後) で PATH を確認する。
54
72
 
55
- 推奨配置: `%USERPROFILE%\dotfiles\global-npm-setup\`
56
- npm グローバル bin: `%AppData%\npm` (通常 PATH に含まれる)
73
+ * 推奨配置 (開発): `%USERPROFILE%\dotfiles\global-npm-setup\`
74
+ * setup ディレクトリ: `%APPDATA%\global-npm`
75
+ * npm グローバル bin: `%AppData%\npm` (通常 PATH に含まれる)
57
76
 
58
77
  ### npm パッケージの導入
59
78
 
60
- 初回 publish 後:
61
-
62
79
  ```sh
63
- npm install -g @s2j/global-npm # CLI 本体の導入(これは一度だけ)
64
- global-npm install # dependencies 一覧をすべて global install
80
+ npm install -g @s2j/global-npm # CLI 本体の導入 (これは一度だけ)
81
+ global-npm add @s2j/docs-linter@^1.0.16 # 任意: ユーザー追加分
82
+ global-npm install # 実効 package.json の dependencies を global install
65
83
  ```
66
84
 
67
- `global-npm install` は、`@s2j/global-npm` 自身 (自己参照 `^2.0.1`) も含め、`dependencies` のキーを列挙して `npm install -g` します。
85
+ 初回の `global-npm install` で `~/.config/global-npm/` (Windows 11では `%APPDATA%\global-npm\`) に実効 `package.json` が生成されます。
86
+ `global-npm install` は、`@s2j/global-npm` 自身 (自己参照) も含め、`dependencies` のキーを列挙して `npm install -g` します。
68
87
 
69
- ### 開発 — リポジトリ clone
88
+ ### 開発: リポジトリ clone
70
89
 
71
90
  ```sh
72
91
  git clone https://github.com/stein2nd/global-npm-setup.git
73
92
  cd global-npm-setup
74
93
  npm link
94
+ GLOBAL_NPM_SETUP_DIR=.sandbox/setup global-npm sync
75
95
  global-npm install
76
96
  ```
77
97
 
78
- ## package.json の役割
98
+ ## ユーザー追加分の管理
99
+
100
+ ```sh
101
+ # 追加分を登録 (range 省略時は npm view で ^x.y.z、失敗時は *)
102
+ global-npm add typescript
103
+ global-npm add eslint --dev
104
+
105
+ # マージ結果を確認 (書き込みなし)
106
+ global-npm sync --dry-run
107
+ ```
108
+
109
+ * upstream (`npm update -g @s2j/global-npm`) 更新後も、ユーザー追加分は消えない。
110
+ * upstream 管理パッケージのうち未 update 分は、次回 `check` 時の sync で新 range に追従する。
111
+ * upstream パッケージをピン留めする場合は、`user-deps.json` の `dependencies` に同名で range を書く。
79
112
 
80
- 同梱の `package.json` の `dependencies` が、グローバルインストール対象パッケージ一覧です。
81
- `check` / `update` / `install` はいずれもこの一覧を参照します。
113
+ ## 使い方
82
114
 
83
- 一覧の変更はリポジトリ更新 → npm publish → `npm update -g @s2j/global-npm` で各環境に反映します。
115
+ 下記については、[使い方](./docs/usage.md) をご覧ください。
84
116
 
117
+ * 各コマンドの役割
118
+ * 定番フロー (`check` → `update` → `install`)
119
+ * 毎回 `sync` を実行する必要があるか
120
+ * upstream 管理分と追加分の衝突が起こった場合
85
121
 
86
122
  ## 日常の更新サイクル
87
123
 
@@ -93,41 +129,44 @@ flowchart LR
93
129
  B -->|No| E["終了"]
94
130
  ```
95
131
 
96
- * **check** … 読み取り専用。何が新しいか (公開日時付き) を見るだけ
97
- * **update** … `@s2j/global-npm` 同梱の `package.json` の `dependencies` バージョン範囲を更新
98
- * **install** … その一覧に従って実際にグローバルパッケージを入れ直す/更新する
99
-
100
- `global-npm install` は、`@s2j/global-npm` 自身 (自己参照 `^2.0.1`) も含め、`dependencies` のキーを列挙して `npm install -g` します。
132
+ * **check:** sync 後に ncu で更新候補を表示。ncu 自体は range を書き換えない。
133
+ * **update:** 実効 package.json の `dependencies` / `devDependencies` の range を更新。
134
+ * **install:** 実効 package.json の **dependencies** のみ global install。
101
135
 
102
136
  ## 開発用 scripts
103
137
 
104
- CLI 実装のデバッグ用に、リポジトリ root から次の npm scripts も使えます。
138
+ CLI 実装のデバッグ用に、リポジトリ root から下記の npm scripts も使えます。
105
139
 
106
140
  ```sh
107
141
  npm run ncu:check
108
142
  npm run ncu:update
109
143
  ```
110
144
 
111
- ## グローバルパッケージ一覧の初期取り込み (任意)
112
-
113
- 手元のグローバル npm パッケージを `dependencies` に反映する作業です。CLI 実行には jq は不要ですが、この作業で jq を使う場合は任意です。
114
-
115
- ```sh
116
- npm ls -g --depth=0 --json > tmp.json
117
- jq '.dependencies | to_entries | map({ key: .key, value: ("^" + (.value.version | tostring)) }) | from_entries' tmp.json > deps.json
118
- jq '.dependencies = input' package.json deps.json > package.tmp.json && mv package.tmp.json package.json
119
- rm tmp.json deps.json
120
- ```
145
+ ## 移行
121
146
 
122
- ## 移行 (v1→ v2)
147
+ ### v1→ v2
123
148
 
124
149
  | v1 | v2 |
125
150
  |----|-----|
126
- | `~/bin/global-npm` (Zsh) | 廃止 — `npm install -g @s2j/global-npm` |
127
- | `install-global.zsh` | 廃止 — `global-npm install` |
151
+ | `~/bin/global-npm` (Zsh) | 廃止: `npm install -g @s2j/global-npm` |
152
+ | `install-global.zsh` | 廃止: `global-npm install` |
128
153
  | `ncu:install` (jq 列挙) | `global-npm install` (Node 列挙) |
129
- | SETUP_DIR = ラッパー配置 dir (`~/bin`) | package root (npm global install 先) を参照 |
154
+
155
+ ### v2.0.x → v2.1
156
+
157
+ | v2.0.x | v2.1 |
158
+ |--------|------|
159
+ | setup = package root | setup = `~/.config/global-npm`。Windows は `%APPDATA%\global-npm` |
160
+ | 同梱 `package.json` を直接 ncu | 実効 package.json を ncu |
161
+ | 3サブコマンド | 5サブコマンド (`sync` / `add` 追加) |
162
+
163
+ 移行手順は、下記のとおりです。
164
+
165
+ 1. `npm update -g @s2j/global-npm` で v2.1に上げる。
166
+ 2. 追加分を `global-npm add …` で登録する。
167
+ 3. `global-npm sync` で 実効 package.json を生成する。
168
+ 4. `global-npm install` で global 環境を同期する。
130
169
 
131
170
  ## ライセンス
132
171
 
133
- GPL-3.0-or-later — 詳細は [LICENSE](./LICENSE) を参照してください。
172
+ GPL-3.0-or-later: 詳細は [LICENSE](./LICENSE) をご覧ください。
@@ -1,19 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { spawnSync } = require('child_process');
4
- const fs = require('fs');
5
4
  const path = require('path');
6
5
 
7
- const setupDir = path.resolve(__dirname, '..');
8
- const pkgPath = path.join(setupDir, 'package.json');
6
+ const { toGlobalInstallSpec } = require('../lib/install-spec.cjs');
7
+ const { ensureSetupDir, readDependencies, readUserDeps, writeJson } = require('../lib/pkg-io.cjs');
8
+ const { resolveSetupContext } = require('../lib/paths.cjs');
9
+ const { parseAddSpec, resolveDefaultRange } = require('../lib/resolve-range.cjs');
10
+ const { formatReport, syncManifest } = require('../lib/sync-manifest.cjs');
11
+
12
+ const packageRoot = path.resolve(__dirname, '..');
13
+ const ctx = resolveSetupContext(packageRoot);
14
+ const { pkgPath } = ctx;
9
15
  const shell = process.platform === 'win32';
10
16
 
11
17
  function usage() {
12
- console.error(`Usage: global-npm <check|update|install>
18
+ console.error(`Usage: global-npm <check|update|install|sync|add>
13
19
 
14
20
  check Check for available updates (ncu -g)
15
21
  update Update version ranges in package.json (ncu -g -u)
16
- install Install dependencies globally (npm install -g <name>@<range>…)`);
22
+ install Install dependencies globally (npm install -g <name>@<range>…)
23
+ sync Merge upstream + user-deps into materialized package.json
24
+ add Add a package to user-deps.json (optional: --dev)`);
17
25
  process.exit(1);
18
26
  }
19
27
 
@@ -31,39 +39,82 @@ function run(command, args) {
31
39
  process.exit(result.status ?? 1);
32
40
  }
33
41
 
34
- function toGlobalInstallSpec(name, versionRange) {
35
- if (typeof versionRange === 'string' && versionRange.trim() !== '') {
36
- return `${name}@${versionRange}`;
42
+ function prepare() {
43
+ ensureSetupDir(ctx);
44
+ return syncManifest(ctx);
45
+ }
46
+
47
+ function handleSync(dryRun) {
48
+ ensureSetupDir(ctx);
49
+ const { changed, report } = syncManifest(ctx, { dryRun });
50
+
51
+ if (dryRun) {
52
+ const lines = formatReport(report);
53
+ if (lines.length === 0) {
54
+ console.error('No changes.');
55
+ } else {
56
+ for (const line of lines) {
57
+ console.error(line);
58
+ }
59
+ }
60
+ process.exit(0);
37
61
  }
38
62
 
39
- return name;
63
+ if (changed) {
64
+ console.error(`Synced materialized manifest: ${pkgPath}`);
65
+ }
66
+
67
+ process.exit(0);
40
68
  }
41
69
 
42
- function readDependencies() {
43
- try {
44
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
45
- return pkg.dependencies ?? {};
46
- } catch (err) {
47
- console.error(`Failed to read ${pkgPath}: ${err.message}`);
70
+ function handleAdd(args) {
71
+ const isDev = args.includes('--dev');
72
+ const positional = args.filter((arg) => arg !== '--dev');
73
+
74
+ if (positional.length !== 1) {
75
+ console.error('Usage: global-npm add <pkg>[@range] [--dev]');
48
76
  process.exit(1);
49
77
  }
78
+
79
+ const { name, range: explicitRange } = parseAddSpec(positional[0]);
80
+ if (!name) {
81
+ console.error('Package name is required.');
82
+ process.exit(1);
83
+ }
84
+
85
+ const range =
86
+ explicitRange !== undefined ? explicitRange : resolveDefaultRange(name);
87
+
88
+ ensureSetupDir(ctx);
89
+ const userDeps = readUserDeps(ctx);
90
+ const section = isDev ? 'devDependencies' : 'dependencies';
91
+ userDeps[section][name] = range;
92
+ writeJson(ctx.userDepsPath, userDeps);
93
+
94
+ syncManifest(ctx);
95
+ console.error(`Added ${name}@${range} to user-deps.json (${section}).`);
96
+ process.exit(0);
50
97
  }
51
98
 
52
99
  const subcommand = process.argv[2];
100
+ const restArgs = process.argv.slice(3);
53
101
 
54
102
  switch (subcommand) {
55
103
  case 'check':
104
+ prepare();
56
105
  run('ncu', ['-g', '--format', 'time', '--packageFile', pkgPath]);
57
106
  break;
58
107
 
59
108
  case 'update':
109
+ prepare();
60
110
  run('ncu', ['-g', '--format', 'time', '-u', '--packageFile', pkgPath]);
61
111
  break;
62
112
 
63
113
  case 'install': {
64
- const dependencies = readDependencies();
65
- const specs = Object.entries(dependencies).map(([name, range]) =>
66
- toGlobalInstallSpec(name, range),
114
+ prepare();
115
+ const dependencies = readDependencies(pkgPath);
116
+ const specs = Object.entries(dependencies).map(([name, versionRange]) =>
117
+ toGlobalInstallSpec(name, versionRange),
67
118
  );
68
119
 
69
120
  if (specs.length === 0) {
@@ -75,6 +126,14 @@ switch (subcommand) {
75
126
  break;
76
127
  }
77
128
 
129
+ case 'sync':
130
+ handleSync(restArgs.includes('--dry-run'));
131
+ break;
132
+
133
+ case 'add':
134
+ handleAdd(restArgs);
135
+ break;
136
+
78
137
  default:
79
138
  usage();
80
139
  }
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ function toGlobalInstallSpec(name, versionRange) {
4
+ if (typeof versionRange === 'string' && versionRange.trim() !== '') {
5
+ return `${name}@${versionRange}`;
6
+ }
7
+
8
+ return name;
9
+ }
10
+
11
+ module.exports = {
12
+ toGlobalInstallSpec,
13
+ };
package/lib/paths.cjs ADDED
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+
6
+ function defaultSetupDir() {
7
+ if (process.platform === 'win32') {
8
+ const appData =
9
+ process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming');
10
+ return path.join(appData, 'global-npm');
11
+ }
12
+
13
+ return path.join(os.homedir(), '.config', 'global-npm');
14
+ }
15
+
16
+ function resolveSetupContext(packageRoot) {
17
+ const setupDir = path.resolve(
18
+ process.env.GLOBAL_NPM_SETUP_DIR?.trim() || defaultSetupDir(),
19
+ );
20
+
21
+ return {
22
+ packageRoot,
23
+ setupDir,
24
+ upstreamPkgPath: path.join(packageRoot, 'package.json'),
25
+ pkgPath: path.join(setupDir, 'package.json'),
26
+ userDepsPath: path.join(setupDir, 'user-deps.json'),
27
+ metaPath: path.join(setupDir, '.upstream-meta.json'),
28
+ };
29
+ }
30
+
31
+ module.exports = {
32
+ defaultSetupDir,
33
+ resolveSetupContext,
34
+ };
package/lib/pkg-io.cjs ADDED
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const EMPTY_USER_DEPS = {
7
+ dependencies: {},
8
+ devDependencies: {},
9
+ };
10
+
11
+ function readJson(filePath) {
12
+ if (!fs.existsSync(filePath)) {
13
+ return null;
14
+ }
15
+
16
+ const raw = fs.readFileSync(filePath, 'utf8');
17
+ return JSON.parse(raw);
18
+ }
19
+
20
+ function writeJson(filePath, data) {
21
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
22
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
23
+ }
24
+
25
+ function ensureSetupDir(ctx) {
26
+ fs.mkdirSync(ctx.setupDir, { recursive: true });
27
+
28
+ if (!fs.existsSync(ctx.userDepsPath)) {
29
+ writeJson(ctx.userDepsPath, { ...EMPTY_USER_DEPS });
30
+ }
31
+ }
32
+
33
+ function readUserDeps(ctx) {
34
+ const data = readJson(ctx.userDepsPath);
35
+ return {
36
+ dependencies: { ...(data?.dependencies ?? {}) },
37
+ devDependencies: { ...(data?.devDependencies ?? {}) },
38
+ };
39
+ }
40
+
41
+ function readDependencies(pkgPath) {
42
+ const pkg = readJson(pkgPath);
43
+ if (!pkg) {
44
+ return {};
45
+ }
46
+
47
+ return pkg.dependencies ?? {};
48
+ }
49
+
50
+ module.exports = {
51
+ EMPTY_USER_DEPS,
52
+ readJson,
53
+ writeJson,
54
+ ensureSetupDir,
55
+ readUserDeps,
56
+ readDependencies,
57
+ };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('child_process');
4
+
5
+ const shell = process.platform === 'win32';
6
+
7
+ function resolveDefaultRange(pkgName, { spawn = spawnSync, log = console.error } = {}) {
8
+ const result = spawn('npm', ['view', pkgName, 'version', '--json'], {
9
+ encoding: 'utf8',
10
+ shell,
11
+ });
12
+
13
+ if (result.status === 0) {
14
+ try {
15
+ const version = JSON.parse(result.stdout.trim());
16
+ if (typeof version === 'string' && version) {
17
+ return `^${version}`;
18
+ }
19
+ } catch {
20
+ // fall through to wildcard fallback
21
+ }
22
+ }
23
+
24
+ log(`Warning: could not resolve latest version for ${pkgName}; using "*".`);
25
+ return '*';
26
+ }
27
+
28
+ function parseAddSpec(arg) {
29
+ if (!arg || typeof arg !== 'string') {
30
+ return { name: '', range: undefined };
31
+ }
32
+
33
+ if (!arg.startsWith('@')) {
34
+ const at = arg.lastIndexOf('@');
35
+ if (at > 0) {
36
+ return { name: arg.slice(0, at), range: arg.slice(at + 1) };
37
+ }
38
+ return { name: arg, range: undefined };
39
+ }
40
+
41
+ const slash = arg.indexOf('/');
42
+ const at = arg.lastIndexOf('@');
43
+ if (at > 0 && at > slash) {
44
+ return { name: arg.slice(0, at), range: arg.slice(at + 1) };
45
+ }
46
+
47
+ return { name: arg, range: undefined };
48
+ }
49
+
50
+ module.exports = {
51
+ resolveDefaultRange,
52
+ parseAddSpec,
53
+ };
@@ -0,0 +1,221 @@
1
+ 'use strict';
2
+
3
+ const { readJson, writeJson } = require('./pkg-io.cjs');
4
+
5
+ const MATERIALIZED_NAME = 'global-npm-user-manifest';
6
+
7
+ function mergeDependencies({ upstream, current, meta, userDeps }) {
8
+ const merged = {};
9
+ const upstreamDeps = upstream.dependencies ?? {};
10
+ const userDepOverrides = userDeps.dependencies ?? {};
11
+ const prevUpstream = meta?.dependencies ?? {};
12
+ const currentDeps = current?.dependencies ?? {};
13
+
14
+ for (const [name, upstreamRange] of Object.entries(upstreamDeps)) {
15
+ if (Object.hasOwn(userDepOverrides, name)) {
16
+ merged[name] = userDepOverrides[name];
17
+ continue;
18
+ }
19
+
20
+ const currentRange = currentDeps[name];
21
+ const prevRange = prevUpstream[name];
22
+
23
+ if (
24
+ currentRange !== undefined &&
25
+ prevRange !== undefined &&
26
+ currentRange !== prevRange
27
+ ) {
28
+ merged[name] = currentRange;
29
+ } else {
30
+ merged[name] = upstreamRange;
31
+ }
32
+ }
33
+
34
+ for (const [name, range] of Object.entries(userDepOverrides)) {
35
+ if (!Object.hasOwn(upstreamDeps, name)) {
36
+ merged[name] = range;
37
+ }
38
+ }
39
+
40
+ for (const [name, range] of Object.entries(currentDeps)) {
41
+ if (Object.hasOwn(merged, name)) {
42
+ continue;
43
+ }
44
+
45
+ const wasUpstream = Object.hasOwn(prevUpstream, name);
46
+ const stillUpstream = Object.hasOwn(upstreamDeps, name);
47
+
48
+ if (wasUpstream && !stillUpstream && !Object.hasOwn(userDepOverrides, name)) {
49
+ continue;
50
+ }
51
+
52
+ merged[name] = range;
53
+ }
54
+
55
+ return merged;
56
+ }
57
+
58
+ function mergeDevDependencies({ current, meta, userDeps }) {
59
+ const merged = {};
60
+ const userDevDeps = userDeps.devDependencies ?? {};
61
+ const prevUserDevDeps = meta?.userDeps?.devDependencies ?? {};
62
+ const currentDevDeps = current?.devDependencies ?? {};
63
+
64
+ for (const [name, userRange] of Object.entries(userDevDeps)) {
65
+ const currentRange = currentDevDeps[name];
66
+ const prevRange = prevUserDevDeps[name];
67
+
68
+ if (
69
+ currentRange !== undefined &&
70
+ prevRange !== undefined &&
71
+ currentRange !== prevRange
72
+ ) {
73
+ merged[name] = currentRange;
74
+ } else {
75
+ merged[name] = userRange;
76
+ }
77
+ }
78
+
79
+ for (const [name, range] of Object.entries(currentDevDeps)) {
80
+ if (Object.hasOwn(merged, name) || Object.hasOwn(userDevDeps, name)) {
81
+ continue;
82
+ }
83
+
84
+ if (Object.hasOwn(prevUserDevDeps, name)) {
85
+ continue;
86
+ }
87
+
88
+ merged[name] = range;
89
+ }
90
+
91
+ return merged;
92
+ }
93
+
94
+ function diffSections(before = {}, after = {}) {
95
+ const added = [];
96
+ const updated = [];
97
+ const removed = [];
98
+
99
+ for (const [name, range] of Object.entries(after)) {
100
+ if (!Object.hasOwn(before, name)) {
101
+ added.push({ name, range });
102
+ } else if (before[name] !== range) {
103
+ updated.push({ name, from: before[name], to: range });
104
+ }
105
+ }
106
+
107
+ for (const name of Object.keys(before)) {
108
+ if (!Object.hasOwn(after, name)) {
109
+ removed.push({ name, range: before[name] });
110
+ }
111
+ }
112
+
113
+ return { added, updated, removed };
114
+ }
115
+
116
+ function buildReport(beforePkg, afterPkg) {
117
+ return {
118
+ dependencies: diffSections(beforePkg?.dependencies, afterPkg.dependencies),
119
+ devDependencies: diffSections(
120
+ beforePkg?.devDependencies,
121
+ afterPkg.devDependencies,
122
+ ),
123
+ };
124
+ }
125
+
126
+ function hasReportChanges(report) {
127
+ for (const section of Object.values(report)) {
128
+ if (section.added.length > 0 || section.updated.length > 0 || section.removed.length > 0) {
129
+ return true;
130
+ }
131
+ }
132
+
133
+ return false;
134
+ }
135
+
136
+ function formatReport(report) {
137
+ const lines = [];
138
+
139
+ for (const [sectionName, section] of Object.entries(report)) {
140
+ for (const { name, range } of section.added) {
141
+ lines.push(`+ ${sectionName} ${name}@${range}`);
142
+ }
143
+ for (const { name, from, to } of section.updated) {
144
+ lines.push(`~ ${sectionName} ${name}: ${from} -> ${to}`);
145
+ }
146
+ for (const { name, range } of section.removed) {
147
+ lines.push(`- ${sectionName} ${name}@${range}`);
148
+ }
149
+ }
150
+
151
+ return lines;
152
+ }
153
+
154
+ function buildMaterializedPkg(merged) {
155
+ return {
156
+ name: MATERIALIZED_NAME,
157
+ private: true,
158
+ dependencies: merged.dependencies,
159
+ devDependencies: merged.devDependencies,
160
+ };
161
+ }
162
+
163
+ function syncManifest(ctx, { dryRun = false } = {}) {
164
+ const upstream = readJson(ctx.upstreamPkgPath);
165
+ if (!upstream) {
166
+ throw new Error(`Failed to read upstream package.json: ${ctx.upstreamPkgPath}`);
167
+ }
168
+
169
+ const userDeps = readJson(ctx.userDepsPath) ?? {
170
+ dependencies: {},
171
+ devDependencies: {},
172
+ };
173
+ const normalizedUserDeps = {
174
+ dependencies: { ...(userDeps.dependencies ?? {}) },
175
+ devDependencies: { ...(userDeps.devDependencies ?? {}) },
176
+ };
177
+
178
+ const current = readJson(ctx.pkgPath);
179
+ const meta = readJson(ctx.metaPath);
180
+
181
+ const merged = {
182
+ dependencies: mergeDependencies({
183
+ upstream,
184
+ current,
185
+ meta,
186
+ userDeps: normalizedUserDeps,
187
+ }),
188
+ devDependencies: mergeDevDependencies({
189
+ current,
190
+ meta,
191
+ userDeps: normalizedUserDeps,
192
+ }),
193
+ };
194
+
195
+ const nextPkg = buildMaterializedPkg(merged);
196
+ const report = buildReport(current, nextPkg);
197
+ const changed = hasReportChanges(report);
198
+
199
+ if (!dryRun) {
200
+ writeJson(ctx.pkgPath, nextPkg);
201
+ writeJson(ctx.metaPath, {
202
+ upstreamVersion: upstream.version,
203
+ dependencies: { ...(upstream.dependencies ?? {}) },
204
+ userDeps: {
205
+ dependencies: { ...normalizedUserDeps.dependencies },
206
+ devDependencies: { ...normalizedUserDeps.devDependencies },
207
+ },
208
+ });
209
+ }
210
+
211
+ return { changed, report, pkgPath: ctx.pkgPath, nextPkg };
212
+ }
213
+
214
+ module.exports = {
215
+ MATERIALIZED_NAME,
216
+ mergeDependencies,
217
+ mergeDevDependencies,
218
+ buildMaterializedPkg,
219
+ syncManifest,
220
+ formatReport,
221
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@s2j/global-npm",
3
- "version": "2.0.3",
3
+ "version": "2.1.1",
4
4
  "description": "Manage globally installed npm packages via package.json with ncu.",
5
5
  "author": "Koutarou ISHIKAWA <stein2nd@gmail.com>",
6
6
  "type": "commonjs",
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "files": [
12
12
  "bin/",
13
+ "lib/",
13
14
  "package.json",
14
15
  "LICENSE",
15
16
  "README.md"
@@ -34,10 +35,10 @@
34
35
  "node": ">=18"
35
36
  },
36
37
  "scripts": {
37
- "test": "node --test test/spec-compliance.test.cjs",
38
+ "test": "node --test test/*.test.cjs",
38
39
  "lint": "eslint .",
39
40
  "lint:fix": "eslint . --fix",
40
- "lint:docs": "s2j-docs-linter --profile base ./README.md ./docs/**/*.md ./docsMod/status.md ./docsMod/modification.md ./.vscode/**/*.md",
41
+ "lint:docs": "s2j-docs-linter --profile base ./README.md ./CHANGELOG.md ./docs/**/*.md ./docsMod/status.md ./docsMod/modification.md ./docsMod/mod-overlay-manifest.md ./.vscode/**/*.md",
41
42
  "format": "prettier --write \"**/*.{cjs,js,mjs,json}\"",
42
43
  "format:check": "prettier --check \"**/*.{cjs,js,mjs,json}\"",
43
44
  "pack": "mkdir -p artifacts && npm pack --pack-destination artifacts",
@@ -55,7 +56,7 @@
55
56
  "typescript": "^5.9.3"
56
57
  },
57
58
  "dependencies": {
58
- "@s2j/global-npm": "^2.0.2",
59
+ "@s2j/global-npm": "^2.1.0",
59
60
  "corepack": "^0.35.0",
60
61
  "editorconfig-cli": "^1.0.0",
61
62
  "envinfo": "^7.21.0",