@ywal123456/jskim 0.3.1 → 0.5.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
@@ -14,14 +14,17 @@ Nunjucks を使った汎用の静的 HTML ビルド環境です。
14
14
  - ファイル監視(`watch`)後の全体再ビルド
15
15
  - ビルド結果のローカル静的サーバー(`serve`)
16
16
  - 開発サーバー(`dev` = build + watch + serve)
17
- - 成功した再ビルド後のページ全体ライブリロード(SSE)
17
+ - 成功した再ビルド後のライブリロード(SSE)
18
+ - 再ビルド失敗時の browser error overlay(`dev` + `liveReload`)
19
+ - 安全な CSS 変更時の stylesheet soft reload(`dev` + `liveReload`)
20
+ - CLI 便利機能(`build --all`、project 省略、`--host` / `--port`、`dev --open`)
18
21
  - CLI binary `jskim`
19
22
  - プロジェクト生成 CLI `create-jskim`
20
23
 
21
24
  ## 現在の非対応範囲
22
25
 
23
- - ブラウザ自動起動
24
- - HMR / CSS だけのホット更新
26
+ - ブラウザ選択 option / `serve --open`
27
+ - HMR / JavaScript module hot replacement
25
28
  - JSON / YAML などの外部データファイル自動読み込み
26
29
  - API / Mock API
27
30
  - 増分ビルド
@@ -71,10 +74,11 @@ npx create-jskim my-project
71
74
  ## CLI
72
75
 
73
76
  ```bash
74
- jskim build <project>
75
- jskim watch <project>
76
- jskim serve <project>
77
- jskim dev <project>
77
+ jskim build [<project>]
78
+ jskim build --all
79
+ jskim watch [<project>]
80
+ jskim serve [<project>] [--host <host>] [--port <port>]
81
+ jskim dev [<project>] [--host <host>] [--port <port>] [--open]
78
82
  ```
79
83
 
80
84
  `package.json` の scripts 例:
@@ -95,6 +99,11 @@ jskim dev <project>
95
99
  - コマンドは実行したディレクトリの `process.cwd()` をプロジェクトルートとして扱う
96
100
  - パッケージのインストール先(`node_modules/@ywal123456/jskim`)を作業空間とはみなさない
97
101
  - プロジェクトルートに `jskim.config.js` が必要
102
+ - 設定内の project が 1 件だけのとき、project 名を省略できる
103
+ - `build --all` は定義順に全 project を順次 build する。1 件でも失敗すれば exit code 1
104
+ - `--host` / `--port` は serve / dev の CLI 上書き(config より優先)。`dev` の config hot reload 中も維持される
105
+ - `dev --open` は listen 成功後に browser を 1 回開く。失敗しても warning のみで dev は継続する
106
+ - option は project の前でも後ろでもよい(例: `jskim dev --port 4000 sample`)
98
107
 
99
108
  ヘルプ / バージョン:
100
109
 
@@ -206,7 +215,7 @@ jskim watch sample
206
215
  - legacy mode: `sourceDir` + `render[].from`、`sourceDir` + `templates[]`、`sourceDir` + `copy[].from`
207
216
  - `dist/`、`node_modules/`、`outputDir` は監視しません
208
217
 
209
- `watch` / `dev` は `jskim.config.js` の変更を検知し、正常なら監視対象を更新して全体ビルドします。`dev` で `outputDir` / `serve.host` / `serve.port` / `dev.liveReload` を変えた場合は process 再起動が必要です。
218
+ `watch` / `dev` は `jskim.config.js` の変更を検知し、正常なら監視対象を更新して全体ビルドします。`dev` で effective な `outputDir` / `serve.host` / `serve.port` / `dev.liveReload` を変えた場合は process 再起動が必要です。CLI の `--host` / `--port` がある場合は、その上書き後の値で再起動要否を判定します。
210
219
 
211
220
  ## serve / dev
212
221
 
@@ -234,9 +243,16 @@ http://127.0.0.1:3000/
234
243
  - `serve` は自動で build / watch を実行しません
235
244
  - サーバールートは `outputDir`(例: `dist/sample`)
236
245
  - `serve` は HTML を変換せず、ライブリロード script を注入しません
237
- - `dev` は成功した再ビルドのあとだけ SSE でページ全体 reload を送ります
238
- - `dev` でも `dist` ファイルへ script は書き込みません
239
- - ブラウザ自動起動はサポートしません
246
+ - `watch` にも browser overlay / CSS soft reload はありません
247
+ - `dev` `dev.liveReload: true` のときだけ browser 向け機能を有効にします
248
+ - 再ビルド失敗時は terminal 診断に加え、browser error overlay を表示します(成功するまで full reload / CSS reload は送りません)
249
+ - CSS ファイルだけが安全に変更された再ビルドでは、ページ全体ではなく stylesheet を更新します
250
+ - CSS 以外の変更や判定が不確実な場合は、従来どおりページ全体 reload します
251
+ - `dev` でも `dist` ファイルへ runtime は書き込みません
252
+ - `jskim serve|dev ... --host` / `--port` で待受を上書きできる(config より優先)
253
+ - ポート衝突時は `--port` の例と `serve.port` 変更を案内する(自動で別 port は選ばない)
254
+ - `jskim dev ... --open` で listen 成功後に browser を 1 回開く(失敗は warning、dev は継続)
255
+ - 厳格な Content Security Policy で inline script または inline style が禁止されている場合、dev の live reload・error overlay・CSS soft reload が動作しないことがあります
240
256
 
241
257
  `/_jskim/live-reload` は `dev.liveReload: true` のときだけ使う内部予約パスです。実際の静的ファイルパスとしては使わないことを推奨します。
242
258
 
package/bin/jskim.js CHANGED
@@ -2,14 +2,13 @@
2
2
  'use strict';
3
3
 
4
4
  const path = require('node:path');
5
- const { getHelpText, getUnknownCommandText } = require('../scripts/commands/help-text');
5
+ const { getHelpText } = require('../scripts/commands/help-text');
6
+ const { parseJskimArgv } = require('../scripts/lib/parse-cli-args');
6
7
  const { runBuildCommand } = require('../scripts/commands/build-command');
7
8
  const { runWatchCommand } = require('../scripts/commands/watch-command');
8
9
  const { runServeCommand } = require('../scripts/commands/serve-command');
9
10
  const { runDevCommand } = require('../scripts/commands/dev-command');
10
11
 
11
- const COMMANDS = new Set(['build', 'watch', 'serve', 'dev']);
12
-
13
12
  function readPackageVersion() {
14
13
  // package インストール先の package.json を読む(作業空間ではない)
15
14
  const pkg = require(path.join(__dirname, '..', 'package.json'));
@@ -28,72 +27,76 @@ function printVersion() {
28
27
  * @param {string[]} argv process.argv 相当(node と script を除く)
29
28
  */
30
29
  async function dispatch(argv) {
31
- const first = argv[0];
32
-
33
- if (!first) {
34
- printHelp();
35
- process.exitCode = 0;
30
+ let parsed;
31
+ try {
32
+ parsed = parseJskimArgv(argv);
33
+ } catch (err) {
34
+ const message = err && err.message ? err.message : String(err);
35
+ console.error(message);
36
+ process.exitCode = 1;
36
37
  return;
37
38
  }
38
39
 
39
- if (first === '--help' || first === '-h' || first === 'help') {
40
+ if (parsed.kind === 'help') {
40
41
  printHelp();
41
42
  process.exitCode = 0;
42
43
  return;
43
44
  }
44
45
 
45
- if (first === '--version' || first === '-v') {
46
+ if (parsed.kind === 'version') {
46
47
  printVersion();
47
48
  process.exitCode = 0;
48
49
  return;
49
50
  }
50
51
 
51
- if (!COMMANDS.has(first)) {
52
- console.error(getUnknownCommandText(first));
53
- process.exitCode = 1;
54
- return;
55
- }
56
-
57
- const projectName = argv[1];
52
+ const command = parsed.command;
53
+ const projectName = parsed.projectName;
54
+ const options = parsed.options;
58
55
  const workspaceRoot = process.cwd();
59
- const usageLine = `jskim ${first} <project>`;
60
- const buildHint = projectName
61
- ? `jskim build ${projectName}`
62
- : 'jskim build <project>';
63
56
 
64
- if (first === 'build') {
57
+ if (command === 'build') {
65
58
  await runBuildCommand({
66
59
  projectName,
60
+ all: options.all,
67
61
  workspaceRoot,
68
- usageLine,
62
+ usageLine: 'jskim build [<project>]',
69
63
  });
70
64
  return;
71
65
  }
72
66
 
73
- if (first === 'watch') {
67
+ if (command === 'watch') {
74
68
  await runWatchCommand({
75
69
  projectName,
76
70
  workspaceRoot,
77
- usageLine,
71
+ usageLine: 'jskim watch [<project>]',
78
72
  });
79
73
  return;
80
74
  }
81
75
 
82
- if (first === 'serve') {
76
+ if (command === 'serve') {
77
+ const buildHint = projectName
78
+ ? `jskim build ${projectName}`
79
+ : 'jskim build [<project>]';
83
80
  await runServeCommand({
84
81
  projectName,
85
82
  workspaceRoot,
86
- usageLine,
83
+ usageLine: 'jskim serve [<project>] [--host <host>] [--port <port>]',
87
84
  buildHint,
85
+ host: options.host,
86
+ port: options.port,
88
87
  });
89
88
  return;
90
89
  }
91
90
 
92
- if (first === 'dev') {
91
+ if (command === 'dev') {
93
92
  await runDevCommand({
94
93
  projectName,
95
94
  workspaceRoot,
96
- usageLine,
95
+ usageLine:
96
+ 'jskim dev [<project>] [--host <host>] [--port <port>] [--open]',
97
+ host: options.host,
98
+ port: options.port,
99
+ open: options.open,
97
100
  });
98
101
  }
99
102
  }
@@ -293,7 +293,23 @@ files mode では出力パスの衝突を検出します。たとえば同じ `f
293
293
  | `serve.host` | 空でない `string` | `'127.0.0.1'` |
294
294
  | `serve.port` | `1` 以上 `65535` 以下の整数 | `3000` |
295
295
 
296
- `serve` は自動ビルドしません。`outputDir` が無い場合は先に `npm run build -- <name>` を実行してください。
296
+ `serve` は自動ビルドしません。`outputDir` が無い場合は先に `jskim build <name>` を実行してください。
297
+
298
+ CLI の `--host` / `--port` は config より優先されます。`dev` では config hot reload 後も CLI override を再適用します。
299
+
300
+ ## build --all
301
+
302
+ `jskim build --all` は `Object.keys(config.projects)` の定義順に全 project を順次 build します。
303
+
304
+ - config は 1 回だけ load する
305
+ - project ごとの resolve / build 失敗があっても、可能な限り次の project を続ける
306
+ - 1 件でも失敗すれば最終 exit code は 1
307
+ - 同一または入れ子の `outputDir` は、どの build も始める前にエラーで中断する
308
+ - `jskim build all` は名前が `all` の project 1 件を build する(`--all` とは別)
309
+
310
+ ## project 名の省略
311
+
312
+ 設定内の project がちょうど 1 件のとき、`build` / `watch` / `serve` / `dev` で project 名を省略できます。0 件または 2 件以上では明示指定が必要です(`build --all` を除く)。
297
313
 
298
314
  ## dev.liveReload
299
315
 
@@ -309,16 +325,21 @@ files mode では出力パスの衝突を検出します。たとえば同じ `f
309
325
 
310
326
  - `GET /_jskim/live-reload` で SSE を提供
311
327
  - HTML レスポンスにだけ client script を一時注入(`dist` には書き込まない)
312
- - 成功した再ビルドのあとだけ `reload` event を送信
328
+ - 成功した再ビルドのあとだけ browser 更新を送信(通常はページ全体 reload)
329
+ - 再ビルド失敗時は browser error overlay を表示(成功するまで reload / CSS 更新は送らない)
330
+ - CSS ファイルだけが安全に変更された場合は、ページ全体ではなく stylesheet を更新
313
331
 
314
332
  `false` のとき:
315
333
 
316
334
  - SSE endpoint は提供しない(404)
317
335
  - HTML への script 注入なし
336
+ - error overlay / CSS soft reload も無効
318
337
  - build + watch + serve 自体は動作
319
338
 
320
339
  `/_jskim/live-reload` は内部予約パスです。プロジェクトの静的ファイルとしては使わないことを推奨します。
321
340
 
341
+ 厳格な Content Security Policy で inline script または inline style が禁止されている場合、dev の live reload・error overlay・CSS soft reload が動作しないことがあります。`watch` と `serve` にはこれらの browser 機能はありません。
342
+
322
343
  ## config hot reload
323
344
 
324
345
  `watch` と `dev` はワークスペースルートの `jskim.config.js` を監視します。
@@ -330,13 +351,15 @@ files mode では出力パスの衝突を検出します。たとえば同じ `f
330
351
  | `watch` | config 変更を hot apply。`outputDir` 変更も可。以前の outputDir は自動削除しない |
331
352
  | `dev` | build / watch 関連設定を hot apply。一部 runtime 設定は再起動が必要 |
332
353
 
333
- `dev` では次の値が変わった candidate config は適用しません。
354
+ `dev` では次の **effective** 値が変わった candidate config は適用しません。
334
355
 
335
356
  - `outputDir`
336
357
  - `serve.host`
337
358
  - `serve.port`
338
359
  - `dev.liveReload`
339
360
 
361
+ CLI の `--host` / `--port` がある場合は、上書き適用後の値で比較します。そのため CLI で port を固定しているあいだに config の `serve.port` だけが変わっても、effective が同じなら再起動警告は出ません。
362
+
340
363
  理由: server bind と serve root を安全に切り替えるには process 再起動が必要で、部分適用すると build output と serve root がずれるためです。
341
364
 
342
365
  ## パス基準のまとめ
@@ -54,12 +54,12 @@ project/
54
54
 
55
55
  `jskim.config.js` は `files: [{ from: 'pages', to: '' }]` を使います。`.njk` は末尾の `.njk` だけを外してレンダリングし、画像などはそのままコピーします。
56
56
 
57
- `package.json` の scripts は bin 名 `jskim` を使います(package name ではありません)。既定の engine dependency は `@ywal123456/jskim`(`^0.3.0`)です。
57
+ `package.json` の scripts は bin 名 `jskim` を使います(package name ではありません)。既定の engine dependency は `@ywal123456/jskim`(`^0.5.0`)です。
58
58
 
59
59
  ```json
60
60
  {
61
61
  "devDependencies": {
62
- "@ywal123456/jskim": "^0.3.0"
62
+ "@ywal123456/jskim": "^0.5.0"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "jskim build sample",
@@ -85,7 +85,7 @@ create package の `package.json` に `jskimEngine` metadata を持ち、生成
85
85
  {
86
86
  "jskimEngine": {
87
87
  "packageName": "@ywal123456/jskim",
88
- "version": "^0.3.0"
88
+ "version": "^0.5.0"
89
89
  }
90
90
  }
91
91
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ywal123456/jskim",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Nunjucks-based static HTML build environment with watch, serve, and live reload",
5
5
  "keywords": [
6
6
  "nunjucks",
package/scripts/build.js CHANGED
@@ -1,11 +1,23 @@
1
1
  'use strict';
2
2
 
3
+ const { parseCommandArgv } = require('./lib/parse-cli-args');
3
4
  const { runBuildCommand } = require('./commands/build-command');
4
5
 
6
+ let parsed;
7
+ try {
8
+ parsed = parseCommandArgv('build', process.argv.slice(2));
9
+ } catch (err) {
10
+ const message = err && err.message ? err.message : String(err);
11
+ console.error(message);
12
+ process.exitCode = 1;
13
+ return;
14
+ }
15
+
5
16
  runBuildCommand({
6
- projectName: process.argv[2],
17
+ projectName: parsed.projectName,
18
+ all: parsed.options.all,
7
19
  workspaceRoot: process.cwd(),
8
- usageLine: 'npm run build -- <project-name>',
20
+ usageLine: 'jskim build [<project>]',
9
21
  }).catch((err) => {
10
22
  const message = err && err.message ? err.message : String(err);
11
23
  console.error(message);
@@ -1,11 +1,18 @@
1
1
  'use strict';
2
2
 
3
- const { buildProject } = require('../lib/build-project');
3
+ const { loadConfig } = require('../lib/load-config');
4
+ const { resolveProject } = require('../lib/resolve-project');
5
+ const { buildProject, runBuild } = require('../lib/build-project');
6
+ const { listProjectNames, selectProjectName } = require('../lib/select-project-name');
7
+ const {
8
+ assertCompatibleOutputDirs,
9
+ } = require('../lib/assert-output-dirs-compatible');
4
10
 
5
11
  /**
6
12
  * build コマンドを実行します。
7
13
  * @param {object} options
8
14
  * @param {string|undefined} options.projectName
15
+ * @param {boolean} [options.all]
9
16
  * @param {string} [options.workspaceRoot]
10
17
  * @param {string} [options.usageLine]
11
18
  * @returns {Promise<object>}
@@ -13,9 +20,24 @@ const { buildProject } = require('../lib/build-project');
13
20
  async function runBuildCommand(options = {}) {
14
21
  const workspaceRoot = options.workspaceRoot || process.cwd();
15
22
  const usageLine =
16
- options.usageLine || 'npm run build -- <project-name>';
23
+ options.usageLine || 'jskim build [<project>]';
17
24
 
18
- return buildProject(options.projectName, {
25
+ if (options.all) {
26
+ return runBuildAllProjects({
27
+ workspaceRoot,
28
+ usageLine,
29
+ });
30
+ }
31
+
32
+ const { config } = loadConfig(workspaceRoot);
33
+ const projectName = selectProjectName({
34
+ config,
35
+ projectName: options.projectName,
36
+ commandName: 'build',
37
+ usageLine,
38
+ });
39
+
40
+ return buildProject(projectName, {
19
41
  workspaceRoot,
20
42
  commandName: 'build',
21
43
  usageLine,
@@ -24,6 +46,94 @@ async function runBuildCommand(options = {}) {
24
46
  });
25
47
  }
26
48
 
49
+ /**
50
+ * @param {object} options
51
+ * @returns {Promise<object>}
52
+ */
53
+ async function runBuildAllProjects({ workspaceRoot, usageLine }) {
54
+ const { config } = loadConfig(workspaceRoot);
55
+ const names = listProjectNames(config);
56
+
57
+ if (names.length === 0) {
58
+ throw new Error(
59
+ `[JSKim] 設定にprojectがありません。\n` +
60
+ `jskim.config.js の projects に1件以上定義してください。\n` +
61
+ `使用方法: jskim build --all`
62
+ );
63
+ }
64
+
65
+ /** @type {object[]} */
66
+ const resolved = [];
67
+ /** @type {Array<{ name: string, stage: string, message: string }>} */
68
+ const failures = [];
69
+ /** @type {string[]} */
70
+ const successes = [];
71
+
72
+ for (const name of names) {
73
+ try {
74
+ const project = resolveProject({
75
+ config,
76
+ workspaceRoot,
77
+ projectName: name,
78
+ commandName: 'build',
79
+ usageLine,
80
+ });
81
+ resolved.push(project);
82
+ } catch (err) {
83
+ const message = err && err.message ? err.message : String(err);
84
+ failures.push({ name, stage: 'resolve', message });
85
+ console.error(message);
86
+ }
87
+ }
88
+
89
+ if (resolved.length > 0) {
90
+ assertCompatibleOutputDirs(resolved);
91
+ }
92
+
93
+ for (const project of resolved) {
94
+ console.log(`[JSKim] project "${project.name}" のbuildを開始します`);
95
+ try {
96
+ await runBuild(project, {
97
+ logTitle: 'ビルドが完了しました',
98
+ includeOutput: true,
99
+ });
100
+ successes.push(project.name);
101
+ } catch (err) {
102
+ const message = err && err.message ? err.message : String(err);
103
+ failures.push({ name: project.name, stage: 'build', message });
104
+ console.error(message);
105
+ }
106
+ }
107
+
108
+ const total = names.length;
109
+ const failCount = failures.length;
110
+ const successCount = successes.length;
111
+
112
+ console.log('');
113
+ console.log(
114
+ `[JSKim] ${total}件中${successCount}件のprojectのbuildが完了しました`
115
+ );
116
+ if (failCount > 0) {
117
+ console.log(`[JSKim] 失敗: ${failCount}件`);
118
+ for (const item of failures) {
119
+ console.log(`- ${item.name} (${item.stage})`);
120
+ }
121
+ const error = new Error(
122
+ `[JSKim] build --all が失敗しました(成功 ${successCount} / 失敗 ${failCount} / 全 ${total})。`
123
+ );
124
+ throw error;
125
+ }
126
+
127
+ return {
128
+ total,
129
+ successCount,
130
+ failCount,
131
+ successes,
132
+ failures,
133
+ };
134
+ }
135
+
27
136
  module.exports = {
28
137
  runBuildCommand,
138
+ runBuildAllProjects,
29
139
  };
@@ -9,12 +9,16 @@ const { registerShutdown } = require('./register-shutdown');
9
9
  * @param {string|undefined} options.projectName
10
10
  * @param {string} [options.workspaceRoot]
11
11
  * @param {string} [options.usageLine]
12
+ * @param {string} [options.host]
13
+ * @param {string|number} [options.port]
14
+ * @param {boolean} [options.open]
12
15
  * @returns {Promise<void>}
13
16
  */
14
17
  async function runDevCommand(options = {}) {
15
18
  const workspaceRoot = options.workspaceRoot || process.cwd();
16
19
  const usageLine =
17
- options.usageLine || 'npm run dev -- <project-name>';
20
+ options.usageLine ||
21
+ 'jskim dev [<project>] [--host <host>] [--port <port>] [--open]';
18
22
 
19
23
  const runtime = createWatchRuntime({
20
24
  mode: 'dev',
@@ -22,6 +26,11 @@ async function runDevCommand(options = {}) {
22
26
  projectName: options.projectName,
23
27
  commandName: 'dev',
24
28
  usageLine,
29
+ cliOverrides: {
30
+ host: options.host,
31
+ port: options.port,
32
+ open: Boolean(options.open),
33
+ },
25
34
  });
26
35
 
27
36
  let stopping = false;
@@ -9,17 +9,28 @@ function getHelpText() {
9
9
  'JSKim - Nunjucksを使用した静的HTMLビルド環境',
10
10
  '',
11
11
  '使用方法:',
12
- ' jskim <command> <project>',
12
+ ' jskim build [<project>]',
13
+ ' jskim build --all',
14
+ ' jskim watch [<project>]',
15
+ ' jskim serve [<project>] [--host <host>] [--port <port>]',
16
+ ' jskim dev [<project>] [--host <host>] [--port <port>] [--open]',
13
17
  '',
14
18
  'コマンド:',
15
- ' build <project> 静的ファイルをビルドします。',
16
- ' watch <project> ファイルの変更を監視して再ビルドします。',
17
- ' serve <project> ビルド済みのファイルを配信します。',
18
- ' dev <project> ビルド・監視・サーバー・ライブリロードを起動します。',
19
+ ' build 静的ファイルをビルドします。',
20
+ ' watch ファイルの変更を監視して再ビルドします。',
21
+ ' serve ビルド済みのファイルを配信します。',
22
+ ' dev ビルド・監視・サーバー・ライブリロードを起動します。',
19
23
  '',
20
24
  'オプション:',
21
- ' -h, --help ヘルプを表示します。',
22
- ' -v, --version バージョンを表示します。',
25
+ ' --all 設定内の全projectを順にbuildします(buildのみ)。',
26
+ ' --host <host> serve / dev の待受ホストを上書きします。',
27
+ ' --port <port> serve / dev の待受ポートを上書きします。',
28
+ ' --open listen成功後にbrowserを1回開きます(devのみ)。',
29
+ ' -h, --help ヘルプを表示します。',
30
+ ' -v, --version バージョンを表示します。',
31
+ '',
32
+ '補足:',
33
+ ' projectを省略できるのは、設定内のprojectが1件だけの場合です。',
23
34
  ].join('\n');
24
35
  }
25
36
 
@@ -33,10 +44,11 @@ function getUnknownCommandText(command) {
33
44
  `[JSKim] 不明なコマンドです: ${command}`,
34
45
  '',
35
46
  '使用できるコマンド:',
36
- ' build <project>',
37
- ' watch <project>',
38
- ' serve <project>',
39
- ' dev <project>',
47
+ ' build [<project>]',
48
+ ' build --all',
49
+ ' watch [<project>]',
50
+ ' serve [<project>] [--host <host>] [--port <port>]',
51
+ ' dev [<project>] [--host <host>] [--port <port>] [--open]',
40
52
  ].join('\n');
41
53
  }
42
54
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  const { loadConfig } = require('../lib/load-config');
4
4
  const { resolveProject } = require('../lib/resolve-project');
5
+ const { selectProjectName } = require('../lib/select-project-name');
6
+ const { applyServeCliOverrides } = require('../lib/apply-serve-cli-overrides');
5
7
  const { createStaticServer } = require('../lib/create-static-server');
6
8
  const { registerShutdown } = require('./register-shutdown');
7
9
  const { toDisplayPath } = require('./path-display');
@@ -14,24 +16,37 @@ const { assertOutputDirReady, formatListenError } = require('./serve-errors');
14
16
  * @param {string} [options.workspaceRoot]
15
17
  * @param {string} [options.usageLine]
16
18
  * @param {string} [options.buildHint]
19
+ * @param {string} [options.host]
20
+ * @param {string|number} [options.port]
17
21
  * @returns {Promise<void>}
18
22
  */
19
23
  async function runServeCommand(options = {}) {
20
24
  const workspaceRoot = options.workspaceRoot || process.cwd();
21
25
  const usageLine =
22
- options.usageLine || 'npm run serve -- <project-name>';
26
+ options.usageLine || 'jskim serve [<project>] [--host <host>] [--port <port>]';
23
27
 
24
28
  const { config } = loadConfig(workspaceRoot);
25
- const project = resolveProject({
29
+ const projectName = selectProjectName({
26
30
  config,
27
- workspaceRoot,
28
31
  projectName: options.projectName,
29
32
  commandName: 'serve',
30
33
  usageLine,
31
34
  });
32
35
 
36
+ let project = resolveProject({
37
+ config,
38
+ workspaceRoot,
39
+ projectName,
40
+ commandName: 'serve',
41
+ usageLine,
42
+ });
43
+ project = applyServeCliOverrides(project, {
44
+ host: options.host,
45
+ port: options.port,
46
+ });
47
+
33
48
  const buildHint =
34
- options.buildHint || `npm run build -- ${project.name}`;
49
+ options.buildHint || `jskim build ${project.name}`;
35
50
  assertOutputDirReady(project, { buildHint });
36
51
 
37
52
  const host = project.serve.host.trim();
@@ -53,6 +68,7 @@ async function runServeCommand(options = {}) {
53
68
  host,
54
69
  port,
55
70
  kind: '静的',
71
+ commandName: 'serve',
56
72
  });
57
73
  }
58
74
 
@@ -10,7 +10,7 @@ const fs = require('node:fs');
10
10
  */
11
11
  function assertOutputDirReady(project, options = {}) {
12
12
  const { name, outputDir, outputDirConfig } = project;
13
- const buildHint = options.buildHint || `npm run build -- ${name}`;
13
+ const buildHint = options.buildHint || `jskim build ${name}`;
14
14
 
15
15
  if (!outputDir || String(outputDir).trim() === '') {
16
16
  throw new Error(
@@ -44,15 +44,32 @@ function assertOutputDirReady(project, options = {}) {
44
44
  }
45
45
  }
46
46
 
47
- function formatListenError(err, { projectName, host, port, kind = '静的' }) {
47
+ /**
48
+ * @param {Error} err
49
+ * @param {object} options
50
+ * @param {string} options.projectName
51
+ * @param {string} options.host
52
+ * @param {number} options.port
53
+ * @param {string} [options.kind]
54
+ * @param {'serve'|'dev'|string} [options.commandName]
55
+ */
56
+ function formatListenError(
57
+ err,
58
+ { projectName, host, port, kind = '静的', commandName }
59
+ ) {
48
60
  const code = err && err.code;
61
+ const command = commandName === 'dev' ? 'dev' : 'serve';
49
62
 
50
63
  if (code === 'EADDRINUSE') {
64
+ const examplePort = suggestAlternatePort(port);
51
65
  return new Error(
52
66
  `[JSKim] ポート ${port} はすでに使用されています。\n` +
53
67
  `プロジェクト: ${projectName}\n` +
54
- `ホスト: ${host}\n\n` +
55
- `jskim.config.js の serve.port を変更してください。`
68
+ `ホスト: ${host}\n` +
69
+ `ポート: ${port}\n\n` +
70
+ `別のportを指定する例:\n` +
71
+ `jskim ${command} ${projectName} --port ${examplePort}\n\n` +
72
+ `または jskim.config.js の serve.port を変更してください。`
56
73
  );
57
74
  }
58
75
 
@@ -76,7 +93,16 @@ function formatListenError(err, { projectName, host, port, kind = '静的' }) {
76
93
  );
77
94
  }
78
95
 
96
+ function suggestAlternatePort(port) {
97
+ const n = Number(port);
98
+ if (Number.isInteger(n) && n >= 1 && n < 65535) {
99
+ return n + 1;
100
+ }
101
+ return 3001;
102
+ }
103
+
79
104
  module.exports = {
80
105
  assertOutputDirReady,
81
106
  formatListenError,
107
+ suggestAlternatePort,
82
108
  };