@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 +22 -4
- package/docs/configuration.md +48 -3
- package/docs/create-jskim.md +3 -3
- package/package.json +1 -1
- package/scripts/commands/dev-command.js +10 -109
- package/scripts/commands/watch-command.js +15 -25
- package/scripts/lib/create-watch-runtime.js +516 -0
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`
|
|
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
|
-
|
|
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
|
|
package/docs/configuration.md
CHANGED
|
@@ -185,8 +185,9 @@ projects: {
|
|
|
185
185
|
|
|
186
186
|
注意:
|
|
187
187
|
|
|
188
|
-
-
|
|
189
|
-
-
|
|
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
|
-
|
|
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
|
|
package/docs/create-jskim.md
CHANGED
|
@@ -39,12 +39,12 @@ project/
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
`package.json` の scripts は bin 名 `jskim` を使います(package name ではありません)。
|
|
42
|
-
既定の engine dependency は `@ywal123456/jskim`(`^0.
|
|
42
|
+
既定の engine dependency は `@ywal123456/jskim`(`^0.2.0`)です。
|
|
43
43
|
|
|
44
44
|
```json
|
|
45
45
|
{
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@ywal123456/jskim": "^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.
|
|
73
|
+
"version": "^0.2.0"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
```
|
package/package.json
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
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
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
22
|
-
|
|
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
|
|
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
|
+
};
|