@ywal123456/jskim 0.1.0 → 0.2.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 CHANGED
@@ -25,14 +25,15 @@ Nunjucks ソースを設定に従って静的 HTML にレンダリングし、as
25
25
 
26
26
  - ブラウザ自動起動
27
27
  - HMR / CSS だけのホット更新
28
- - HTML 移行
29
28
  - JSON データの自動読み込み
30
29
  - API / Mock API
31
- - 実際の npm registry publish(手順は準備済み)
32
- - config の hot reload
33
30
  - 増分ビルド
34
31
  - SPA fallback / proxy
35
32
 
33
+ 既存 HTML の自動 import / migration は JSKim core の責任範囲外です。
34
+ 既存 source の移行は利用者が project に合わせて行います。
35
+ 将来必要になった場合も、JSKim 本体ではなく独立した tool / package として検討します。
36
+
36
37
  ## パッケージの役割
37
38
 
38
39
  | 名称 | 種類 | 役割 |
@@ -43,28 +44,38 @@ Nunjucks ソースを設定に従って静的 HTML にレンダリングし、as
43
44
 
44
45
  製品名は **JSKim**、CLI binary は **`jskim`**、npm engine package は **`@ywal123456/jskim`** です。package 名と binary 名を混同しないでください。
45
46
 
46
- Package 名・CLI 構造は公開名に合わせてあります。**このリポジトリ作業では registry への publish は行っていません**。registry からのインストールは publish 後に可能です。scoped engine は公開 MIT package として配布する予定です。
47
+ `@ywal123456/jskim` `create-jskim` npm registry で公開済みの MIT package です。
47
48
 
48
49
  詳細:
49
50
 
50
51
  - [docs/create-jskim.md](docs/create-jskim.md)
51
- - [docs/publishing.md](docs/publishing.md)
52
-
53
- ## パッケージとしての利用
52
+ - [docs/publishing.md](docs/publishing.md)(maintainer 向け release 手順)
54
53
 
55
- 公開後のインストール例:
54
+ ## インストール
56
55
 
57
56
  ```bash
58
57
  npm install --save-dev @ywal123456/jskim
59
58
  ```
60
59
 
60
+ ## 新規 project の作成
61
+
61
62
  ```bash
62
- jskim build sample
63
- jskim watch sample
64
- jskim serve sample
65
- jskim dev sample
63
+ npm create jskim@latest
64
+ ```
66
65
 
67
- npx jskim build sample
66
+ 代替:
67
+
68
+ ```bash
69
+ npx create-jskim my-project
70
+ ```
71
+
72
+ ## CLI
73
+
74
+ ```bash
75
+ jskim build <project>
76
+ jskim watch <project>
77
+ jskim serve <project>
78
+ jskim dev <project>
68
79
  ```
69
80
 
70
81
  `package.json` の scripts 例:
@@ -83,11 +94,11 @@ npx jskim build sample
83
94
  要点:
84
95
 
85
96
  - npm package 名は `@ywal123456/jskim`(scoped)
86
- - インストール後の CLI binary 名は `jskim`(変更なし)
97
+ - インストール後の CLI binary 名は `jskim`
87
98
  - コマンドは実行したディレクトリの `process.cwd()` をプロジェクトルートとして扱う
88
99
  - パッケージのインストール先(`node_modules/@ywal123456/jskim`)を作業空間とはみなさない
89
100
  - プロジェクトルートに `jskim.config.js` が必要
90
- - `create-jskim` package と `npm create jskim@latest` コマンドは維持します
101
+
91
102
  ヘルプ / バージョン:
92
103
 
93
104
  ```bash
@@ -97,18 +108,13 @@ jskim --version
97
108
 
98
109
  ## プロジェクト生成(create-jskim)
99
110
 
100
- 公開後の例:
101
-
102
111
  ```bash
103
112
  npm create jskim@latest
104
113
  # または
105
114
  npx create-jskim my-project
106
115
  ```
107
116
 
108
- ローカル / 開発時:
109
-
110
117
  ```bash
111
- create-jskim my-project
112
118
  cd my-project
113
119
  npm install
114
120
  npm run dev
@@ -116,7 +122,6 @@ npm run dev
116
122
 
117
123
  - 自動で `npm install` / `git init` は実行しません
118
124
  - 空ではない既存ディレクトリは上書きしません
119
- - 実際の registry インストールは publish 後に利用できます
120
125
 
121
126
  ## このリポジトリでの開発コマンド
122
127
 
@@ -167,9 +172,24 @@ jskim watch sample
167
172
 
168
173
  - 関連する変更はすべて **全体ビルド** です(増分 render / 単一 asset copy なし)。
169
174
  - `build.clean: true` のとき、再ビルドで outputDir を消して作り直すため、ソース削除も結果に反映されます。
170
- - `jskim.config.js` を変更した場合は **watch / dev を再起動** してください(config hot reload なし)。
175
+ - `jskim.config.js` の変更は **watch / dev が検知** し、正常なら監視対象を更新して全体ビルドします。
176
+ - 設定の読み込み / 検証に失敗しても process は終了せず、以前の正常な設定を継続します。
177
+ - `serve` は config を監視しません。設定変更後は serve process の再起動が必要です。
171
178
  - 終了: `Ctrl+C`(SIGINT)または SIGTERM → `[JSKim] ウォッチを停止しました。`
172
179
 
180
+ #### config hot reload(watch / dev)
181
+
182
+ - 対象ファイル: ワークスペースルートの `jskim.config.js`
183
+ - 正常な変更: watcher 再構成 → 全体ビルド
184
+ - 不正な設定: 以前の正常な設定を維持し、修正後に自動で再試行
185
+ - `watch` では `outputDir` 変更も適用できます(以前の outputDir は自動削除しません)
186
+ - `dev` では次の変更は process 再起動が必要です:
187
+ - `outputDir`
188
+ - `serve.host`
189
+ - `serve.port`
190
+ - `dev.liveReload`
191
+ - `dev` で対応できる設定変更が成功し、build も成功した場合だけ browser reload を 1 回送ります
192
+
173
193
  debounce の既定値は `watch.debounce: 150`(ms)で、プロジェクトごとに上書きできます。詳細は [docs/configuration.md](docs/configuration.md) を参照してください。
174
194
 
175
195
  ## 静的サーバー(serve)
@@ -234,7 +254,8 @@ http://127.0.0.1:3000/
234
254
 
235
255
  `dev.liveReload: false` にすると SSE endpoint と script 注入を無効化できます。
236
256
 
237
- 設定を変えた場合は `dev` を再起動してください。
257
+ `jskim.config.js` の build / watch 関連設定は hot reload されます。
258
+ `outputDir` / `serve.host` / `serve.port` / `dev.liveReload` を変えた場合は `dev` を再起動してください。
238
259
 
239
260
  終了: `Ctrl+C` → `[JSKim] 開発サーバーを停止しました。`
240
261
 
@@ -185,8 +185,9 @@ projects: {
185
185
 
186
186
  注意:
187
187
 
188
- - この段階では `jskim.config.js` 自体は watch しません
189
- - 設定を変えた場合は watch コマンドを再起動してください
188
+ - `watch` / `dev` 実行中は `jskim.config.js` 自体も監視します
189
+ - 正常な設定変更は監視対象の再構成と全体ビルドに反映されます
190
+ - 設定の読み込み / 検証エラー時は以前の正常な設定を継続します
190
191
 
191
192
  ## serve.host
192
193
 
@@ -295,7 +296,51 @@ projects: {
295
296
 
296
297
  `dev.host` / `dev.port` / `dev.debounce` は用意しません。
297
298
 
298
- 設定変更後は実行中の watcher/server は自動再構成されないため、`dev` / `watch` / `serve` を再起動してください。
299
+ ## config hot reload
300
+
301
+ `watch` と `dev` はワークスペースルートの `jskim.config.js` を監視します。
302
+
303
+ ### コマンド別
304
+
305
+ | コマンド | 動作 |
306
+ |----------|------|
307
+ | `build` | 実行時に config を 1 回読み込む。監視しない |
308
+ | `serve` | 実行時に config を 1 回読み込む。監視しない。変更後は process 再起動が必要 |
309
+ | `watch` | config 変更を hot apply。`outputDir` 変更も可。以前の outputDir は自動削除しない |
310
+ | `dev` | build / watch 関連設定を hot apply。一部 runtime 設定は再起動が必要 |
311
+
312
+ ### 正常な reload
313
+
314
+ 1. config 変更を検知
315
+ 2. fresh load(CommonJS require cache を `jskim.config.js` について削除)
316
+ 3. merge / project resolve / validation
317
+ 4. watcher 再構成
318
+ 5. 全体ビルド
319
+
320
+ ### エラー時
321
+
322
+ | 状況 | 動作 |
323
+ |------|------|
324
+ | config load / resolve / validation エラー | 以前の正常な設定を維持。process は終了しない |
325
+ | 新しい設定適用後の build エラー | 新しい設定 / watcher を維持。process は終了しない。修正後に再試行 |
326
+ | config 一時削除(unlink) | 以前の設定を継続。再作成後に reload |
327
+
328
+ ### `dev` で再起動が必要な設定
329
+
330
+ 次の値が変わった candidate config は適用しません。
331
+
332
+ - `outputDir`
333
+ - `serve.host`
334
+ - `serve.port`
335
+ - `dev.liveReload`
336
+
337
+ 理由: server bind と serve root を安全に切り替えるには process 再起動が必要で、部分適用すると build output と serve root がずれるためです。
338
+
339
+ ### 制限
340
+
341
+ - config hot reload は常に **全体ビルド** です(増分 build ではありません)
342
+ - `jskim.config.js` が require した別 helper module だけの変更は自動反映しません。config を保存し直すか process を再起動してください
343
+ - `node_modules` や JSKim package 自身の require cache は一括削除しません
299
344
 
300
345
  ## パス基準のまとめ
301
346
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  `create-jskim` は JSKim の作業空間を新規作成する **独立 scaffold package** です。
4
4
 
5
- エンジン CLI(`jskim build` など)とは別 package として管理します。
5
+ エンジン CLI(`jskim build` など)とは別 package として管理します。npm registry で公開済みです。
6
6
 
7
7
  ## 役割
8
8
 
@@ -18,7 +18,14 @@
18
18
  | `jskim` | CLI binary |
19
19
  | `@ywal123456/jskim` | npm engine package |
20
20
  | `create-jskim` | npm creator package |
21
- | `npm create jskim@latest` | ユーザー向け生成コマンド(維持) |
21
+ | `npm create jskim@latest` | ユーザー向け生成コマンド |
22
+
23
+ ## 使い方
24
+
25
+ ```bash
26
+ npm create jskim@latest
27
+ npx create-jskim my-project
28
+ ```
22
29
 
23
30
  ## 生成結果
24
31
 
@@ -32,12 +39,12 @@ project/
32
39
  ```
33
40
 
34
41
  `package.json` の scripts は bin 名 `jskim` を使います(package name ではありません)。
35
- 既定の engine dependency は `@ywal123456/jskim`(`^0.1.0`)です。
42
+ 既定の engine dependency は `@ywal123456/jskim`(`^0.2.0`)です。
36
43
 
37
44
  ```json
38
45
  {
39
46
  "devDependencies": {
40
- "@ywal123456/jskim": "^0.1.0"
47
+ "@ywal123456/jskim": "^0.2.0"
41
48
  },
42
49
  "scripts": {
43
50
  "build": "jskim build sample",
@@ -63,18 +70,11 @@ create package の `package.json` に `jskimEngine` metadata を持ち、生成
63
70
  {
64
71
  "jskimEngine": {
65
72
  "packageName": "@ywal123456/jskim",
66
- "version": "^0.1.0"
73
+ "version": "^0.2.0"
67
74
  }
68
75
  }
69
76
  ```
70
77
 
71
78
  生成プロジェクトへ License は強制しません。
72
79
 
73
- ## 公開コマンド(publish 後)
74
-
75
- ```bash
76
- npm create jskim@latest
77
- npx create-jskim my-project
78
- ```
79
-
80
- Package 名・CLI 構造は上記に合わせて準備済みです。registry への実際の publish は別手順です([publishing.md](./publishing.md))。
80
+ Maintainer 向けの release / publish 手順は [publishing.md](./publishing.md) を参照してください。
@@ -1,18 +1,22 @@
1
1
  # 公開手順(publishing)
2
2
 
3
- この文書は **将来の実際の publish 手順** をまとめたものです。
3
+ この文書は **maintainer 向けの release / publish 手順** です。
4
4
  開発リポジトリの通常作業では `npm publish` を実行しません。
5
5
 
6
- ## 前提
6
+ ## 現在公開中
7
7
 
8
- - npm アカウントと認証(2FA ポリシーを含む)
9
- - 公開 package 名
10
- - engine: `@ywal123456/jskim`(scoped・公開 MIT)
11
- - creator: `create-jskim`(unscoped)
8
+ ```text
9
+ engine: @ywal123456/jskim
10
+ creator: create-jskim
11
+ CLI binary: jskim / create-jskim
12
+ license: MIT
13
+ ```
14
+
15
+ 初回公開(v0.1.0)は完了しています。以降の patch / minor release でも、同じ手順で engine を先に、creator を後に公開します。
12
16
 
13
17
  ### 過去の失敗(参考)
14
18
 
15
- unscoped `jskim@0.1.0` の初回 publish は、npm の package name similarity ポリシーにより **E403** で拒否されました。registry には反映されていません。現在の engine 公開名は `@ywal123456/jskim` です。
19
+ unscoped 名での初回 engine publish は、npm の package name similarity ポリシーにより拒否されました。現在の engine 公開名は `@ywal123456/jskim` です。
16
20
 
17
21
  ### 確定済み metadata
18
22
 
@@ -38,15 +42,7 @@ Root `package.json` の `publishConfig`:
38
42
  }
39
43
  ```
40
44
 
41
- `publishConfig` があっても、scoped engine の初回 publish では手動コマンドの `--access public` を省略しないでください。
42
-
43
- ### 残りの運用判断
44
-
45
- ```text
46
- - 実際の npm publish 時点
47
- - npm アカウントのログイン状態
48
- - npm 2FA 状態
49
- ```
45
+ scoped engine publish では手動コマンドの `--access public` を省略しないでください。
50
46
 
51
47
  ## 認証とセキュリティ
52
48
 
@@ -55,51 +51,41 @@ Root `package.json` の `publishConfig`:
55
51
  - token をソースコードや文書に記録しないでください
56
52
  - `.npmrc` の認証情報を repository に含めないでください
57
53
  - publish 直前に `npm whoami` でログイン状態を確認してください
58
- - 公開前に会社固有情報・secret・内部 URL が残っていないことを確認してください
54
+ - 公開作業の前に会社固有情報・secret・内部 URL が残っていないことを確認してください
59
55
 
60
- ## 推奨順序
56
+ ## 推奨順序(patch / minor)
61
57
 
62
58
  Engine を先に publish します。生成プロジェクトの `package.json` が `@ywal123456/jskim` を参照するため、creator だけ先に公開すると生成直後の `npm install` が失敗し得ます。
63
59
 
64
- 1. npm account と認証状態を確認する
65
- 2. package 名を再確認する(`@ywal123456/jskim` / `create-jskim`)
66
- 3. リポジトリ root `npm test` を実行する
67
- 4. engine の `npm pack` / `npm publish --dry-run --access public` を確認する
68
- 5. creator `npm pack` / `npm publish --dry-run` を確認する
69
- 6. **engine を先に publish** する(`--access public` 必須)
60
+ 1. version を決定する
61
+ 2. リポジトリ root で `npm test` を実行する
62
+ 3. engine / creator `npm pack` を確認する
63
+ 4. `npm publish --dry-run` を確認する
64
+ 5. Git commit / push する
65
+ 6. **engine を先に publish** する(`--access public`)
70
66
  7. registry から engine をインストールできることを確認する
71
67
  8. creator を publish する
72
68
  9. `npm create jskim@latest` を検証する
69
+ 10. Git tag と GitHub Release を作成する
73
70
 
74
- ## コマンド例(ユーザーが実行)
71
+ 同じ name / version を再 publish できると仮定しないでください。
75
72
 
76
- リポジトリ root(PowerShell):
73
+ ## コマンド例(PowerShell
74
+
75
+ Engine(リポジトリ root):
77
76
 
78
77
  ```powershell
79
78
  npm.cmd whoami --registry=https://registry.npmjs.org
80
- npm.cmd view @ywal123456/jskim name version --registry=https://registry.npmjs.org
81
- npm.cmd view create-jskim name version --registry=https://registry.npmjs.org
82
79
  npm.cmd test
83
80
  npm.cmd publish --dry-run --access public --registry=https://registry.npmjs.org
84
81
  npm.cmd publish --access public --registry=https://registry.npmjs.org
85
82
  ```
86
83
 
87
- 期待する成功出力:
88
-
89
- ```text
90
- + @ywal123456/jskim@0.1.0
91
- ```
92
-
93
- Registry 確認:
84
+ Registry / インストール確認:
94
85
 
95
86
  ```powershell
96
- npm.cmd view @ywal123456/jskim@0.1.0 name version license bin repository --registry=https://registry.npmjs.org
97
- ```
98
-
99
- 外部インストール:
100
-
101
- ```powershell
102
- npm.cmd install --save-dev @ywal123456/jskim@0.1.0 --registry=https://registry.npmjs.org
87
+ npm.cmd view @ywal123456/jskim name version dist-tags --registry=https://registry.npmjs.org
88
+ npm.cmd install --save-dev @ywal123456/jskim --registry=https://registry.npmjs.org
103
89
  ```
104
90
 
105
91
  Creator:
@@ -110,25 +96,19 @@ npm.cmd publish --dry-run --registry=https://registry.npmjs.org
110
96
  npm.cmd publish --registry=https://registry.npmjs.org
111
97
  ```
112
98
 
113
- 期待する成功出力:
114
-
115
- ```text
116
- + create-jskim@0.1.0
117
- ```
118
-
119
99
  最終ユーザー検証:
120
100
 
121
101
  ```powershell
122
102
  npm.cmd create jskim@latest generated-project
123
- npx create-jskim my-project
124
103
  ```
125
104
 
126
105
  ## 注意
127
106
 
128
- - unscoped engine 向けの `npm publish`(`--access public` なし・旧 package 名)は使わないでください
129
- - lifecycle script(`prepublishOnly` など)は、creator を別 cwd から publish する構成との相性を考えて必須化していません
107
+ - unscoped engine 向けの旧 package 名では publish しません
108
+ - lifecycle script(`prepublishOnly` など)は必須化していません
130
109
  - publish 前チェックリストとして **手動の `npm test`** を必須とします
131
110
  - token 生成・login・owner / dist-tag 変更は運用者が責任を持って行います
132
111
  - 生成プロジェクトへ License を強制しません(ユーザー側で決定)
133
112
  - creator は unscoped public package のため、creator `package.json` に `access: public` は追加していません
134
- - dry-run 成功は、creator similarity 審査通過を保証しません
113
+ - 文書のみの修正でも npm package README を更新する場合は patch version を使います
114
+ - 既存の公開 tag(例: `v0.1.0`)を移動または再作成しません
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ywal123456/jskim",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Nunjucks-based static HTML build environment with watch, serve, and live reload",
5
5
  "keywords": [
6
6
  "nunjucks",
@@ -1,13 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { loadConfig } = require('../lib/load-config');
4
- const { resolveProject } = require('../lib/resolve-project');
5
- const { createProjectWatcher } = require('../lib/create-project-watcher');
6
- const { createStaticServer } = require('../lib/create-static-server');
7
- const { createLiveReload } = require('../lib/create-live-reload');
3
+ const { createWatchRuntime } = require('../lib/create-watch-runtime');
8
4
  const { registerShutdown } = require('./register-shutdown');
9
- const { toDisplayPath } = require('./path-display');
10
- const { formatListenError } = require('./serve-errors');
11
5
 
12
6
  /**
13
7
  * dev コマンドを実行します。
@@ -22,51 +16,15 @@ async function runDevCommand(options = {}) {
22
16
  const usageLine =
23
17
  options.usageLine || 'npm run dev -- <project-name>';
24
18
 
25
- const { config } = loadConfig(workspaceRoot);
26
- const project = resolveProject({
27
- config,
19
+ const runtime = createWatchRuntime({
20
+ mode: 'dev',
28
21
  workspaceRoot,
29
22
  projectName: options.projectName,
30
23
  commandName: 'dev',
31
24
  usageLine,
32
25
  });
33
26
 
34
- const host = project.serve.host.trim();
35
- const port = project.serve.port;
36
- const liveReloadEnabled = project.dev.liveReload;
37
- const outputDisplay = toDisplayPath(project.outputDir, workspaceRoot);
38
-
39
- const liveReload = createLiveReload({
40
- projectName: project.name,
41
- enabled: liveReloadEnabled,
42
- });
43
-
44
- const staticServer = createStaticServer({
45
- rootDir: project.outputDir,
46
- host,
47
- port,
48
- projectName: project.name,
49
- handleInternalRequest: (req, res, meta) =>
50
- liveReload.handleRequest(req, res, meta),
51
- transformHtml: liveReloadEnabled
52
- ? (html) => liveReload.injectHtml(html)
53
- : undefined,
54
- });
55
-
56
- const projectWatcher = createProjectWatcher(project, {
57
- runInitialBuild: true,
58
- logChanges: true,
59
- });
60
-
61
- // 成功した再ビルドのあとだけ reload(初回はクライアント未接続のため不要)
62
- projectWatcher.on('build:success', ({ initial }) => {
63
- if (!initial && liveReloadEnabled) {
64
- liveReload.broadcastReload();
65
- }
66
- });
67
-
68
27
  let stopping = false;
69
- let watcherStarted = false;
70
28
 
71
29
  async function shutdown() {
72
30
  if (stopping) {
@@ -75,21 +33,7 @@ async function runDevCommand(options = {}) {
75
33
  stopping = true;
76
34
 
77
35
  try {
78
- if (watcherStarted) {
79
- await projectWatcher.close();
80
- }
81
- } catch {
82
- // 終了時エラーは無視
83
- }
84
-
85
- try {
86
- liveReload.close();
87
- } catch {
88
- // 終了時エラーは無視
89
- }
90
-
91
- try {
92
- await staticServer.stop();
36
+ await runtime.close();
93
37
  } catch {
94
38
  // 終了時エラーは無視
95
39
  }
@@ -98,65 +42,22 @@ async function runDevCommand(options = {}) {
98
42
  process.exit(0);
99
43
  }
100
44
 
101
- async function failStartup(err) {
45
+ registerShutdown(shutdown);
46
+
47
+ try {
48
+ await runtime.start();
49
+ } catch (err) {
102
50
  const message = err && err.message ? err.message : String(err);
103
51
  console.error(message);
104
-
105
- try {
106
- if (watcherStarted) {
107
- await projectWatcher.close();
108
- }
109
- } catch {
110
- // ignore
111
- }
112
-
113
- try {
114
- liveReload.close();
115
- } catch {
116
- // ignore
117
- }
118
-
119
52
  try {
120
- await staticServer.stop();
53
+ await runtime.close();
121
54
  } catch {
122
55
  // ignore
123
56
  }
124
-
125
57
  // cleanup 完了後に終了する。
126
58
  // IPC 接続があると exitCode だけではプロセスが残るため明示終了する。
127
59
  process.exit(1);
128
60
  }
129
-
130
- registerShutdown(shutdown);
131
-
132
- // 初回 build → server → watcher
133
- await projectWatcher.start({ watchFiles: false });
134
- watcherStarted = true;
135
-
136
- try {
137
- await staticServer.start();
138
- } catch (err) {
139
- await failStartup(
140
- formatListenError(err, {
141
- projectName: project.name,
142
- host,
143
- port,
144
- kind: '開発',
145
- })
146
- );
147
- return;
148
- }
149
-
150
- await projectWatcher.startWatching();
151
-
152
- console.log('[JSKim] 開発サーバーを起動しました。');
153
- console.log(`プロジェクト: ${project.name}`);
154
- console.log(`ルート: ${outputDisplay}`);
155
- console.log(`URL: ${staticServer.url}`);
156
- console.log(
157
- `ライブリロード: ${liveReloadEnabled ? '有効' : '無効'}`
158
- );
159
- console.log('終了するには Ctrl+C を押してください。');
160
61
  }
161
62
 
162
63
  module.exports = {
@@ -1,8 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { loadConfig } = require('../lib/load-config');
4
- const { resolveProject } = require('../lib/resolve-project');
5
- const { createProjectWatcher } = require('../lib/create-project-watcher');
3
+ const { createWatchRuntime } = require('../lib/create-watch-runtime');
6
4
  const { registerShutdown } = require('./register-shutdown');
7
5
 
8
6
  /**
@@ -18,33 +16,14 @@ async function runWatchCommand(options = {}) {
18
16
  const usageLine =
19
17
  options.usageLine || 'npm run watch -- <project-name>';
20
18
 
21
- const { config } = loadConfig(workspaceRoot);
22
- const project = resolveProject({
23
- config,
19
+ const runtime = createWatchRuntime({
20
+ mode: 'watch',
24
21
  workspaceRoot,
25
22
  projectName: options.projectName,
26
23
  commandName: 'watch',
27
24
  usageLine,
28
25
  });
29
26
 
30
- const projectWatcher = createProjectWatcher(project, {
31
- runInitialBuild: true,
32
- logChanges: true,
33
- });
34
-
35
- projectWatcher.on('ready', ({ displayPaths, debounceMs }) => {
36
- console.log(`[JSKim] プロジェクトを監視しています: ${project.name}`);
37
- console.log('パス:');
38
- for (const display of displayPaths) {
39
- console.log(`- ${display}`);
40
- }
41
- console.log('');
42
- console.log(`Debounce: ${debounceMs}ms`);
43
- console.log('終了するには Ctrl+C を押してください。');
44
- });
45
-
46
- await projectWatcher.start();
47
-
48
27
  let stopping = false;
49
28
 
50
29
  async function shutdown() {
@@ -54,7 +33,7 @@ async function runWatchCommand(options = {}) {
54
33
  stopping = true;
55
34
 
56
35
  try {
57
- await projectWatcher.close();
36
+ await runtime.close();
58
37
  } catch {
59
38
  // 終了時エラーは無視
60
39
  }
@@ -64,6 +43,17 @@ async function runWatchCommand(options = {}) {
64
43
  }
65
44
 
66
45
  registerShutdown(shutdown);
46
+
47
+ try {
48
+ await runtime.start();
49
+ } catch (err) {
50
+ try {
51
+ await runtime.close();
52
+ } catch {
53
+ // ignore
54
+ }
55
+ throw err;
56
+ }
67
57
  }
68
58
 
69
59
  module.exports = {
@@ -0,0 +1,516 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const chokidar = require('chokidar');
6
+ const { loadConfig, CONFIG_FILENAME } = require('./load-config');
7
+ const { resolveProject } = require('./resolve-project');
8
+ const { createProjectWatcher } = require('./create-project-watcher');
9
+ const { createStaticServer } = require('./create-static-server');
10
+ const { createLiveReload } = require('./create-live-reload');
11
+ const { formatListenError } = require('../commands/serve-errors');
12
+
13
+ const DEV_RESTART_KEYS = [
14
+ 'outputDir',
15
+ 'serve.host',
16
+ 'serve.port',
17
+ 'dev.liveReload',
18
+ ];
19
+
20
+ /**
21
+ * watch / dev 共通の実行ランタイムです。
22
+ * jskim.config.js の hot reload と project watcher 再構成を担当します。
23
+ *
24
+ * @param {object} options
25
+ * @param {'watch'|'dev'} options.mode
26
+ * @param {string} options.workspaceRoot
27
+ * @param {string|undefined} options.projectName
28
+ * @param {string} [options.commandName]
29
+ * @param {string} [options.usageLine]
30
+ * @returns {{ start: Function, close: Function }}
31
+ */
32
+ function createWatchRuntime(options) {
33
+ const mode = options.mode;
34
+ const workspaceRoot = options.workspaceRoot || process.cwd();
35
+ const projectName = options.projectName;
36
+ const commandName = options.commandName || mode;
37
+ const usageLine =
38
+ options.usageLine || `npm run ${commandName} -- <project-name>`;
39
+
40
+ let currentProject = null;
41
+ let configPath = null;
42
+ let projectWatcher = null;
43
+ let configWatcher = null;
44
+ let liveReload = null;
45
+ let staticServer = null;
46
+ let liveReloadEnabled = false;
47
+
48
+ let stopping = false;
49
+ let started = false;
50
+ let configDebounceTimer = null;
51
+ let configReloadPromise = null;
52
+ let pendingConfigReload = false;
53
+ let sourceBuilding = false;
54
+
55
+ function resolveCurrentProject() {
56
+ const loaded = loadConfig(workspaceRoot);
57
+ const project = resolveProject({
58
+ config: loaded.config,
59
+ workspaceRoot,
60
+ projectName,
61
+ commandName,
62
+ usageLine,
63
+ });
64
+ return {
65
+ project,
66
+ configPath: loaded.configPath,
67
+ };
68
+ }
69
+
70
+ async function start() {
71
+ if (started) {
72
+ return;
73
+ }
74
+ started = true;
75
+
76
+ const resolved = resolveCurrentProject();
77
+ currentProject = resolved.project;
78
+ configPath = resolved.configPath;
79
+
80
+ if (mode === 'dev') {
81
+ await startDevSession(currentProject, { initial: true });
82
+ } else {
83
+ await startWatchSession(currentProject, { initial: true });
84
+ }
85
+
86
+ beginConfigWatching();
87
+ }
88
+
89
+ async function startWatchSession(project, { initial }) {
90
+ projectWatcher = createProjectWatcher(project, {
91
+ runInitialBuild: true,
92
+ logChanges: true,
93
+ });
94
+
95
+ wireSourceBuildTracking(projectWatcher);
96
+
97
+ if (initial) {
98
+ projectWatcher.on('ready', ({ displayPaths, debounceMs }) => {
99
+ console.log(`[JSKim] プロジェクトを監視しています: ${project.name}`);
100
+ console.log('パス:');
101
+ for (const display of displayPaths) {
102
+ console.log(`- ${display}`);
103
+ }
104
+ console.log('');
105
+ console.log(`Debounce: ${debounceMs}ms`);
106
+ console.log('終了するには Ctrl+C を押してください。');
107
+ });
108
+ }
109
+
110
+ await projectWatcher.start();
111
+ }
112
+
113
+ async function startDevSession(project, { initial }) {
114
+ liveReloadEnabled = project.dev.liveReload;
115
+ const host = project.serve.host.trim();
116
+ const port = project.serve.port;
117
+ const outputDisplay = toDisplayPath(project.outputDir, workspaceRoot);
118
+
119
+ liveReload = createLiveReload({
120
+ projectName: project.name,
121
+ enabled: liveReloadEnabled,
122
+ });
123
+
124
+ staticServer = createStaticServer({
125
+ rootDir: project.outputDir,
126
+ host,
127
+ port,
128
+ projectName: project.name,
129
+ handleInternalRequest: (req, res, meta) =>
130
+ liveReload.handleRequest(req, res, meta),
131
+ transformHtml: liveReloadEnabled
132
+ ? (html) => liveReload.injectHtml(html)
133
+ : undefined,
134
+ });
135
+
136
+ projectWatcher = createProjectWatcher(project, {
137
+ runInitialBuild: true,
138
+ logChanges: true,
139
+ });
140
+
141
+ wireSourceBuildTracking(projectWatcher);
142
+ wireDevSourceReload(projectWatcher);
143
+
144
+ await projectWatcher.start({ watchFiles: false });
145
+
146
+ try {
147
+ await staticServer.start();
148
+ } catch (err) {
149
+ const formatted = formatListenError(err, {
150
+ projectName: project.name,
151
+ host,
152
+ port,
153
+ kind: '開発',
154
+ });
155
+ await cleanupDevComponents();
156
+ throw formatted;
157
+ }
158
+
159
+ await projectWatcher.startWatching();
160
+
161
+ if (initial) {
162
+ console.log('[JSKim] 開発サーバーを起動しました。');
163
+ console.log(`プロジェクト: ${project.name}`);
164
+ console.log(`ルート: ${outputDisplay}`);
165
+ console.log(`URL: ${staticServer.url}`);
166
+ console.log(
167
+ `ライブリロード: ${liveReloadEnabled ? '有効' : '無効'}`
168
+ );
169
+ console.log('終了するには Ctrl+C を押してください。');
170
+ }
171
+ }
172
+
173
+ function wireSourceBuildTracking(watcher) {
174
+ watcher.on('build:start', () => {
175
+ sourceBuilding = true;
176
+ });
177
+ watcher.on('build:success', () => {
178
+ sourceBuilding = false;
179
+ maybeRunPendingConfigReload();
180
+ });
181
+ watcher.on('build:failure', () => {
182
+ sourceBuilding = false;
183
+ maybeRunPendingConfigReload();
184
+ });
185
+ }
186
+
187
+ function wireDevSourceReload(watcher) {
188
+ watcher.on('build:success', ({ initial }) => {
189
+ if (!initial && liveReloadEnabled && liveReload) {
190
+ liveReload.broadcastReload();
191
+ }
192
+ });
193
+ }
194
+
195
+ function beginConfigWatching() {
196
+ if (configWatcher || stopping) {
197
+ return;
198
+ }
199
+
200
+ configWatcher = chokidar.watch(configPath, {
201
+ ignoreInitial: true,
202
+ persistent: true,
203
+ awaitWriteFinish: {
204
+ stabilityThreshold: 50,
205
+ pollInterval: 25,
206
+ },
207
+ });
208
+
209
+ configWatcher.on('all', (eventName) => {
210
+ if (
211
+ eventName === 'add' ||
212
+ eventName === 'change' ||
213
+ eventName === 'unlink'
214
+ ) {
215
+ queueConfigReload(eventName);
216
+ }
217
+ });
218
+
219
+ configWatcher.on('error', (err) => {
220
+ const message = err && err.message ? err.message : String(err);
221
+ console.error(`[JSKim] 設定ファイルの監視エラー: ${message}`);
222
+ });
223
+ }
224
+
225
+ function queueConfigReload(eventName) {
226
+ if (stopping) {
227
+ return;
228
+ }
229
+
230
+ if (eventName === 'unlink') {
231
+ console.log(
232
+ `[JSKim] 設定ファイルが一時的に削除されました。以前の正常な設定を継続します。`
233
+ );
234
+ } else {
235
+ console.log('[JSKim] 設定ファイルの変更を検出しました。');
236
+ }
237
+
238
+ if (configDebounceTimer) {
239
+ clearTimeout(configDebounceTimer);
240
+ }
241
+
242
+ const debounceMs =
243
+ currentProject && currentProject.watch
244
+ ? currentProject.watch.debounce
245
+ : 150;
246
+
247
+ configDebounceTimer = setTimeout(() => {
248
+ configDebounceTimer = null;
249
+ requestConfigReload();
250
+ }, debounceMs);
251
+ }
252
+
253
+ function requestConfigReload() {
254
+ if (stopping) {
255
+ return;
256
+ }
257
+
258
+ if (configReloadPromise || sourceBuilding) {
259
+ pendingConfigReload = true;
260
+ return;
261
+ }
262
+
263
+ configReloadPromise = (async () => {
264
+ try {
265
+ await performConfigReload();
266
+ } finally {
267
+ configReloadPromise = null;
268
+ if (!stopping && pendingConfigReload) {
269
+ pendingConfigReload = false;
270
+ requestConfigReload();
271
+ }
272
+ }
273
+ })();
274
+ }
275
+
276
+ function maybeRunPendingConfigReload() {
277
+ if (!stopping && pendingConfigReload && !configReloadPromise) {
278
+ pendingConfigReload = false;
279
+ requestConfigReload();
280
+ }
281
+ }
282
+
283
+ async function performConfigReload() {
284
+ if (stopping) {
285
+ return;
286
+ }
287
+
288
+ if (!fs.existsSync(configPath)) {
289
+ // unlink 後まだ復元されていない
290
+ return;
291
+ }
292
+
293
+ let candidate;
294
+ try {
295
+ const loaded = loadConfig(workspaceRoot);
296
+ candidate = resolveProject({
297
+ config: loaded.config,
298
+ workspaceRoot,
299
+ projectName,
300
+ commandName,
301
+ usageLine,
302
+ });
303
+ } catch (err) {
304
+ const message = err && err.message ? err.message : String(err);
305
+ console.error('[JSKim] 設定ファイルの再読み込みに失敗しました。');
306
+ console.error(message);
307
+ console.error('[JSKim] 以前の正常な設定を継続します。');
308
+ return;
309
+ }
310
+
311
+ if (mode === 'dev') {
312
+ const restartKeys = getRestartRequiredChanges(currentProject, candidate);
313
+ if (restartKeys.length > 0) {
314
+ console.warn(
315
+ '[JSKim] 次の設定変更を反映するにはdev processの再起動が必要です:'
316
+ );
317
+ for (const key of restartKeys) {
318
+ console.warn(`- ${key}`);
319
+ }
320
+ console.warn('[JSKim] 以前の正常な設定を継続します。');
321
+ return;
322
+ }
323
+ }
324
+
325
+ console.log('[JSKim] 設定を再読み込みしました。');
326
+
327
+ let buildSucceeded = false;
328
+ try {
329
+ await replaceProjectWatcher(candidate, {
330
+ onInitialBuildSuccess: () => {
331
+ buildSucceeded = true;
332
+ },
333
+ onInitialBuildFailure: () => {
334
+ buildSucceeded = false;
335
+ },
336
+ });
337
+ currentProject = candidate;
338
+ console.log('[JSKim] 監視対象を更新しました。');
339
+ if (mode === 'watch') {
340
+ console.log('パス:');
341
+ for (const display of projectWatcher.displayPaths) {
342
+ console.log(`- ${display}`);
343
+ }
344
+ console.log(`Debounce: ${projectWatcher.debounceMs}ms`);
345
+ }
346
+ } catch (err) {
347
+ const message = err && err.message ? err.message : String(err);
348
+ console.error('[JSKim] 設定の再読み込み後に監視の更新に失敗しました。');
349
+ console.error(message);
350
+ console.error('[JSKim] 以前の正常な設定を継続します。');
351
+ return;
352
+ }
353
+
354
+ if (!buildSucceeded) {
355
+ console.error(
356
+ '[JSKim] 設定の再読み込み後にbuildが失敗しました。監視は新しい設定で継続します。'
357
+ );
358
+ return;
359
+ }
360
+
361
+ if (mode === 'dev' && liveReloadEnabled && liveReload) {
362
+ liveReload.broadcastReload();
363
+ }
364
+ }
365
+
366
+ async function replaceProjectWatcher(project, hooks = {}) {
367
+ if (projectWatcher) {
368
+ try {
369
+ await projectWatcher.close();
370
+ } catch {
371
+ // reload 時の close エラーは無視
372
+ }
373
+ projectWatcher = null;
374
+ }
375
+
376
+ projectWatcher = createProjectWatcher(project, {
377
+ runInitialBuild: true,
378
+ logChanges: true,
379
+ });
380
+
381
+ wireSourceBuildTracking(projectWatcher);
382
+ if (mode === 'dev') {
383
+ wireDevSourceReload(projectWatcher);
384
+ }
385
+
386
+ const onSuccess = ({ initial }) => {
387
+ if (initial && hooks.onInitialBuildSuccess) {
388
+ hooks.onInitialBuildSuccess();
389
+ }
390
+ };
391
+ const onFailure = ({ initial }) => {
392
+ if (initial && hooks.onInitialBuildFailure) {
393
+ hooks.onInitialBuildFailure();
394
+ }
395
+ };
396
+ projectWatcher.on('build:success', onSuccess);
397
+ projectWatcher.on('build:failure', onFailure);
398
+
399
+ await projectWatcher.start();
400
+ }
401
+
402
+ async function cleanupDevComponents() {
403
+ if (projectWatcher) {
404
+ try {
405
+ await projectWatcher.close();
406
+ } catch {
407
+ // ignore
408
+ }
409
+ projectWatcher = null;
410
+ }
411
+ if (liveReload) {
412
+ try {
413
+ liveReload.close();
414
+ } catch {
415
+ // ignore
416
+ }
417
+ liveReload = null;
418
+ }
419
+ if (staticServer) {
420
+ try {
421
+ await staticServer.stop();
422
+ } catch {
423
+ // ignore
424
+ }
425
+ staticServer = null;
426
+ }
427
+ }
428
+
429
+ async function close() {
430
+ if (stopping) {
431
+ return;
432
+ }
433
+ stopping = true;
434
+
435
+ if (configDebounceTimer) {
436
+ clearTimeout(configDebounceTimer);
437
+ configDebounceTimer = null;
438
+ }
439
+
440
+ if (configWatcher) {
441
+ try {
442
+ await configWatcher.close();
443
+ } catch {
444
+ // ignore
445
+ }
446
+ configWatcher = null;
447
+ }
448
+
449
+ if (configReloadPromise) {
450
+ try {
451
+ await configReloadPromise;
452
+ } catch {
453
+ // ignore
454
+ }
455
+ }
456
+
457
+ if (mode === 'dev') {
458
+ await cleanupDevComponents();
459
+ } else if (projectWatcher) {
460
+ try {
461
+ await projectWatcher.close();
462
+ } catch {
463
+ // ignore
464
+ }
465
+ projectWatcher = null;
466
+ }
467
+ }
468
+
469
+ return {
470
+ start,
471
+ close,
472
+ get project() {
473
+ return currentProject;
474
+ },
475
+ get configFileName() {
476
+ return CONFIG_FILENAME;
477
+ },
478
+ };
479
+ }
480
+
481
+ /**
482
+ * @param {object} previous
483
+ * @param {object} next
484
+ * @returns {string[]}
485
+ */
486
+ function getRestartRequiredChanges(previous, next) {
487
+ const changes = [];
488
+ if (previous.outputDir !== next.outputDir) {
489
+ changes.push('outputDir');
490
+ }
491
+ if (previous.serve.host !== next.serve.host) {
492
+ changes.push('serve.host');
493
+ }
494
+ if (previous.serve.port !== next.serve.port) {
495
+ changes.push('serve.port');
496
+ }
497
+ if (previous.dev.liveReload !== next.dev.liveReload) {
498
+ changes.push('dev.liveReload');
499
+ }
500
+ return changes;
501
+ }
502
+
503
+ function toDisplayPath(abs, workspaceRoot) {
504
+ const resolved = path.resolve(abs);
505
+ const rel = path.relative(workspaceRoot, resolved);
506
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
507
+ return resolved.split(path.sep).join('/');
508
+ }
509
+ return rel.split(path.sep).join('/');
510
+ }
511
+
512
+ module.exports = {
513
+ createWatchRuntime,
514
+ getRestartRequiredChanges,
515
+ DEV_RESTART_KEYS,
516
+ };