@ywal123456/jskim 0.1.1 → 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,13 +25,15 @@ Nunjucks ソースを設定に従って静的 HTML にレンダリングし、as
25
25
 
26
26
  - ブラウザ自動起動
27
27
  - HMR / CSS だけのホット更新
28
- - HTML 移行
29
28
  - JSON データの自動読み込み
30
29
  - API / Mock API
31
- - config の hot reload
32
30
  - 増分ビルド
33
31
  - SPA fallback / proxy
34
32
 
33
+ 既存 HTML の自動 import / migration は JSKim core の責任範囲外です。
34
+ 既存 source の移行は利用者が project に合わせて行います。
35
+ 将来必要になった場合も、JSKim 本体ではなく独立した tool / package として検討します。
36
+
35
37
  ## パッケージの役割
36
38
 
37
39
  | 名称 | 種類 | 役割 |
@@ -170,9 +172,24 @@ jskim watch sample
170
172
 
171
173
  - 関連する変更はすべて **全体ビルド** です(増分 render / 単一 asset copy なし)。
172
174
  - `build.clean: true` のとき、再ビルドで outputDir を消して作り直すため、ソース削除も結果に反映されます。
173
- - `jskim.config.js` を変更した場合は **watch / dev を再起動** してください(config hot reload なし)。
175
+ - `jskim.config.js` の変更は **watch / dev が検知** し、正常なら監視対象を更新して全体ビルドします。
176
+ - 設定の読み込み / 検証に失敗しても process は終了せず、以前の正常な設定を継続します。
177
+ - `serve` は config を監視しません。設定変更後は serve process の再起動が必要です。
174
178
  - 終了: `Ctrl+C`(SIGINT)または SIGTERM → `[JSKim] ウォッチを停止しました。`
175
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
+
176
193
  debounce の既定値は `watch.debounce: 150`(ms)で、プロジェクトごとに上書きできます。詳細は [docs/configuration.md](docs/configuration.md) を参照してください。
177
194
 
178
195
  ## 静的サーバー(serve)
@@ -237,7 +254,8 @@ http://127.0.0.1:3000/
237
254
 
238
255
  `dev.liveReload: false` にすると SSE endpoint と script 注入を無効化できます。
239
256
 
240
- 設定を変えた場合は `dev` を再起動してください。
257
+ `jskim.config.js` の build / watch 関連設定は hot reload されます。
258
+ `outputDir` / `serve.host` / `serve.port` / `dev.liveReload` を変えた場合は `dev` を再起動してください。
241
259
 
242
260
  終了: `Ctrl+C` → `[JSKim] 開発サーバーを停止しました。`
243
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
 
@@ -39,12 +39,12 @@ project/
39
39
  ```
40
40
 
41
41
  `package.json` の scripts は bin 名 `jskim` を使います(package name ではありません)。
42
- 既定の engine dependency は `@ywal123456/jskim`(`^0.1.0`)です。
42
+ 既定の engine dependency は `@ywal123456/jskim`(`^0.2.0`)です。
43
43
 
44
44
  ```json
45
45
  {
46
46
  "devDependencies": {
47
- "@ywal123456/jskim": "^0.1.0"
47
+ "@ywal123456/jskim": "^0.2.0"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "jskim build sample",
@@ -70,7 +70,7 @@ create package の `package.json` に `jskimEngine` metadata を持ち、生成
70
70
  {
71
71
  "jskimEngine": {
72
72
  "packageName": "@ywal123456/jskim",
73
- "version": "^0.1.0"
73
+ "version": "^0.2.0"
74
74
  }
75
75
  }
76
76
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ywal123456/jskim",
3
- "version": "0.1.1",
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
+ };