@s2j/global-npm 2.0.3 → 2.1.0
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 +64 -34
- package/bin/global-npm.cjs +77 -18
- package/lib/install-spec.cjs +13 -0
- package/lib/paths.cjs +34 -0
- package/lib/pkg-io.cjs +57 -0
- package/lib/resolve-range.cjs +53 -0
- package/lib/sync-manifest.cjs +221 -0
- package/package.json +4 -3
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
|
|
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 → 物理的なアーカイブを再生成
|
|
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
|
-
|
|
26
|
+
## しくみ (v2.1)
|
|
27
|
+
|
|
28
|
+
| レイヤ | 場所 | 役割 |
|
|
29
|
+
|--------|------|------|
|
|
30
|
+
| Upstream 正本 | `@s2j/global-npm` 同梱 `package.json` | 公式 `dependencies` 一覧 |
|
|
31
|
+
| ユーザー overlay | `$SETUP_DIR/user-deps.json` | 追加分、ピン留め |
|
|
32
|
+
| 物理的なアーカイブ | `$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
|
-
|
|
40
|
-
|
|
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,19 +70,20 @@ Node.js v18以上 (LTS 推奨) が未導入の場合は、先にインストー
|
|
|
52
70
|
3. `node --version`、`npm --version` でバージョンを確認する。
|
|
53
71
|
4. PowerShell を再起動後、`global-npm check` (導入後) で PATH を確認する。
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
64
|
-
global-npm
|
|
80
|
+
npm install -g @s2j/global-npm # CLI 本体の導入 (これは一度だけ)
|
|
81
|
+
global-npm add @s2j/docs-linter@^1.0.16 # 任意: ユーザー追加分
|
|
82
|
+
global-npm install # 物理的なアーカイブ化 dependencies を global install
|
|
65
83
|
```
|
|
66
84
|
|
|
67
|
-
`global-npm install`
|
|
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
88
|
### 開発 — リポジトリ clone
|
|
70
89
|
|
|
@@ -72,16 +91,24 @@ global-npm install # dependencies 一覧をすべて global install
|
|
|
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
|
-
##
|
|
98
|
+
## ユーザー追加分の管理
|
|
79
99
|
|
|
80
|
-
|
|
81
|
-
|
|
100
|
+
```sh
|
|
101
|
+
# 追加分を登録 (range 省略時は npm view で ^x.y.z、失敗時は *)
|
|
102
|
+
global-npm add typescript
|
|
103
|
+
global-npm add eslint --dev
|
|
82
104
|
|
|
83
|
-
|
|
105
|
+
# マージ結果を確認 (書き込みなし)
|
|
106
|
+
global-npm sync --dry-run
|
|
107
|
+
```
|
|
84
108
|
|
|
109
|
+
* upstream (`npm update -g @s2j/global-npm`) 更新後も、ユーザー追加分は消えない。
|
|
110
|
+
* upstream 管理パッケージのうち未 update 分は、次回 `check` 時の sync で新 range に追従する。
|
|
111
|
+
* upstream パッケージをピン留めする場合は、`user-deps.json` の `dependencies` に同名で range を書く。
|
|
85
112
|
|
|
86
113
|
## 日常の更新サイクル
|
|
87
114
|
|
|
@@ -93,40 +120,43 @@ flowchart LR
|
|
|
93
120
|
B -->|No| E["終了"]
|
|
94
121
|
```
|
|
95
122
|
|
|
96
|
-
* **check
|
|
97
|
-
* **update
|
|
98
|
-
* **install**
|
|
99
|
-
|
|
100
|
-
`global-npm install` は、`@s2j/global-npm` 自身 (自己参照 `^2.0.1`) も含め、`dependencies` のキーを列挙して `npm install -g` します。
|
|
123
|
+
* **check:** sync 後に ncu で更新候補を表示。ncu 自体は range を書き換えない。
|
|
124
|
+
* **update:** 物理的なアーカイブ化 `dependencies` / `devDependencies` の range を更新。
|
|
125
|
+
* **install:** 物理的なアーカイブ化 **dependencies** のみ global install。
|
|
101
126
|
|
|
102
127
|
## 開発用 scripts
|
|
103
128
|
|
|
104
|
-
CLI 実装のデバッグ用に、リポジトリ root
|
|
129
|
+
CLI 実装のデバッグ用に、リポジトリ root から下記の npm scripts も使えます。
|
|
105
130
|
|
|
106
131
|
```sh
|
|
107
132
|
npm run ncu:check
|
|
108
133
|
npm run ncu:update
|
|
109
134
|
```
|
|
110
135
|
|
|
111
|
-
##
|
|
136
|
+
## 移行
|
|
112
137
|
|
|
113
|
-
|
|
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
|
-
```
|
|
121
|
-
|
|
122
|
-
## 移行 (v1→ v2)
|
|
138
|
+
### v1→ v2
|
|
123
139
|
|
|
124
140
|
| v1 | v2 |
|
|
125
141
|
|----|-----|
|
|
126
142
|
| `~/bin/global-npm` (Zsh) | 廃止 — `npm install -g @s2j/global-npm` |
|
|
127
143
|
| `install-global.zsh` | 廃止 — `global-npm install` |
|
|
128
144
|
| `ncu:install` (jq 列挙) | `global-npm install` (Node 列挙) |
|
|
129
|
-
|
|
145
|
+
|
|
146
|
+
### v2.0.x → v2.1
|
|
147
|
+
|
|
148
|
+
| v2.0.x | v2.1 |
|
|
149
|
+
|--------|------|
|
|
150
|
+
| setup = package root | setup = `~/.config/global-npm`。Windows は `%APPDATA%\global-npm` |
|
|
151
|
+
| 同梱 `package.json` を直接 ncu | 物理的なアーカイブを ncu |
|
|
152
|
+
| 3サブコマンド | 5サブコマンド (`sync` / `add` 追加) |
|
|
153
|
+
|
|
154
|
+
移行手順は、下記のとおりです。
|
|
155
|
+
|
|
156
|
+
1. `npm update -g @s2j/global-npm` で v2.1に上げる。
|
|
157
|
+
2. 追加分を `global-npm add …` で登録する。
|
|
158
|
+
3. `global-npm sync` で 物理的なアーカイブを生成する。
|
|
159
|
+
4. `global-npm install` で global 環境を同期する。
|
|
130
160
|
|
|
131
161
|
## ライセンス
|
|
132
162
|
|
package/bin/global-npm.cjs
CHANGED
|
@@ -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
|
|
8
|
-
const
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
63
|
+
if (changed) {
|
|
64
|
+
console.error(`Synced materialized manifest: ${pkgPath}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
process.exit(0);
|
|
40
68
|
}
|
|
41
69
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.error(
|
|
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
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
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
|
+
"version": "2.1.0",
|
|
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
|
|
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",
|