@ywal123456/jskim 0.4.0 → 0.5.1
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 +19 -8
- package/bin/jskim.js +32 -29
- package/docs/configuration.md +20 -2
- package/docs/create-jskim.md +20 -3
- package/package.json +1 -1
- package/scripts/build.js +14 -2
- package/scripts/commands/build-command.js +113 -3
- package/scripts/commands/dev-command.js +10 -1
- package/scripts/commands/help-text.js +23 -11
- package/scripts/commands/serve-command.js +20 -4
- package/scripts/commands/serve-errors.js +30 -4
- package/scripts/commands/watch-command.js +1 -1
- package/scripts/dev.js +16 -2
- package/scripts/lib/apply-serve-cli-overrides.js +96 -0
- package/scripts/lib/assert-output-dirs-compatible.js +88 -0
- package/scripts/lib/create-watch-runtime.js +70 -9
- package/scripts/lib/open-browser.js +102 -0
- package/scripts/lib/parse-cli-args.js +216 -0
- package/scripts/lib/select-project-name.js +64 -0
- package/scripts/serve.js +21 -2
- package/scripts/watch.js +13 -2
|
@@ -14,7 +14,7 @@ const { registerShutdown } = require('./register-shutdown');
|
|
|
14
14
|
async function runWatchCommand(options = {}) {
|
|
15
15
|
const workspaceRoot = options.workspaceRoot || process.cwd();
|
|
16
16
|
const usageLine =
|
|
17
|
-
options.usageLine || '
|
|
17
|
+
options.usageLine || 'jskim watch [<project>]';
|
|
18
18
|
|
|
19
19
|
const runtime = createWatchRuntime({
|
|
20
20
|
mode: 'watch',
|
package/scripts/dev.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { parseCommandArgv } = require('./lib/parse-cli-args');
|
|
3
4
|
const { runDevCommand } = require('./commands/dev-command');
|
|
4
5
|
|
|
6
|
+
let parsed;
|
|
7
|
+
try {
|
|
8
|
+
parsed = parseCommandArgv('dev', 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
|
runDevCommand({
|
|
6
|
-
projectName:
|
|
17
|
+
projectName: parsed.projectName,
|
|
7
18
|
workspaceRoot: process.cwd(),
|
|
8
|
-
usageLine: '
|
|
19
|
+
usageLine: 'jskim dev [<project>] [--host <host>] [--port <port>] [--open]',
|
|
20
|
+
host: parsed.options.host,
|
|
21
|
+
port: parsed.options.port,
|
|
22
|
+
open: parsed.options.open,
|
|
9
23
|
}).catch((err) => {
|
|
10
24
|
const message = err && err.message ? err.message : String(err);
|
|
11
25
|
console.error(message);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { formatConfigValidationError } = require('./format-diagnostic');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CLI --host / --port を resolved project に適用した新しいオブジェクトを返します。
|
|
7
|
+
* 元の project / config は変更しません。
|
|
8
|
+
*
|
|
9
|
+
* @param {object} project
|
|
10
|
+
* @param {{ host?: string, port?: string|number }} [overrides]
|
|
11
|
+
* @returns {object}
|
|
12
|
+
*/
|
|
13
|
+
function applyServeCliOverrides(project, overrides = {}) {
|
|
14
|
+
if (!project || typeof project !== 'object') {
|
|
15
|
+
throw new Error('[JSKim] projectが不正です。');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const hasHost = overrides.host !== undefined;
|
|
19
|
+
const hasPort = overrides.port !== undefined;
|
|
20
|
+
if (!hasHost && !hasPort) {
|
|
21
|
+
return project;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const nextServe = {
|
|
25
|
+
...(project.serve || {}),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (hasHost) {
|
|
29
|
+
nextServe.host = overrides.host;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (hasPort) {
|
|
33
|
+
nextServe.port = coercePort(overrides.port, project.name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
validateServeOverride(nextServe, project.name);
|
|
37
|
+
return {
|
|
38
|
+
...project,
|
|
39
|
+
serve: nextServe,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function coercePort(value, projectName) {
|
|
44
|
+
if (typeof value === 'number') {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
const text = String(value).trim();
|
|
48
|
+
if (text === '' || !/^-?\d+$/.test(text)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
formatConfigValidationError({
|
|
51
|
+
projectName,
|
|
52
|
+
configKey: 'CLI --port',
|
|
53
|
+
detail: '1から65535までの整数を指定してください。',
|
|
54
|
+
received: String(value),
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return Number(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateServeOverride(serve, projectName) {
|
|
62
|
+
const host = serve && serve.host;
|
|
63
|
+
const port = serve && serve.port;
|
|
64
|
+
|
|
65
|
+
if (typeof host !== 'string' || host.trim() === '') {
|
|
66
|
+
throw new Error(
|
|
67
|
+
formatConfigValidationError({
|
|
68
|
+
projectName,
|
|
69
|
+
configKey: 'CLI --host / serve.host',
|
|
70
|
+
detail: '空でない文字列を指定してください。',
|
|
71
|
+
received: String(host),
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
typeof port !== 'number' ||
|
|
78
|
+
!Number.isInteger(port) ||
|
|
79
|
+
port < 1 ||
|
|
80
|
+
port > 65535
|
|
81
|
+
) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
formatConfigValidationError({
|
|
84
|
+
projectName,
|
|
85
|
+
configKey: 'CLI --port / serve.port',
|
|
86
|
+
detail: '1から65535までの整数を指定してください。',
|
|
87
|
+
received: String(port),
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
applyServeCliOverrides,
|
|
95
|
+
coercePort,
|
|
96
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* --all 用に、resolve済みprojectのoutputDir衝突を検査します。
|
|
7
|
+
*
|
|
8
|
+
* @param {Array<{ name: string, outputDir: string }>} projects
|
|
9
|
+
*/
|
|
10
|
+
function assertCompatibleOutputDirs(projects) {
|
|
11
|
+
const items = Array.isArray(projects) ? projects : [];
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
14
|
+
for (let j = i + 1; j < items.length; j += 1) {
|
|
15
|
+
const a = items[i];
|
|
16
|
+
const b = items[j];
|
|
17
|
+
const relation = classifyOutputDirRelation(a.outputDir, b.outputDir);
|
|
18
|
+
if (!relation) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const label =
|
|
23
|
+
relation === 'same'
|
|
24
|
+
? '同じoutputDir'
|
|
25
|
+
: '入れ子(祖先/子孫)のoutputDir';
|
|
26
|
+
|
|
27
|
+
throw new Error(
|
|
28
|
+
`[JSKim] --all でbuildできません: project間のoutputDirが衝突しています。\n` +
|
|
29
|
+
`理由: ${label} のため、clean付きbuildで他projectの成果物を消す恐れがあります。\n\n` +
|
|
30
|
+
`project: ${a.name}\n` +
|
|
31
|
+
`outputDir: ${toPosix(a.outputDir)}\n\n` +
|
|
32
|
+
`project: ${b.name}\n` +
|
|
33
|
+
`outputDir: ${toPosix(b.outputDir)}\n\n` +
|
|
34
|
+
`各projectのoutputDirを重複・入れ子にならないよう変更してください。`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} left
|
|
42
|
+
* @param {string} right
|
|
43
|
+
* @returns {'same'|'nested'|null}
|
|
44
|
+
*/
|
|
45
|
+
function classifyOutputDirRelation(left, right) {
|
|
46
|
+
const a = path.resolve(left);
|
|
47
|
+
const b = path.resolve(right);
|
|
48
|
+
if (samePath(a, b)) {
|
|
49
|
+
return 'same';
|
|
50
|
+
}
|
|
51
|
+
if (isStrictAncestor(a, b) || isStrictAncestor(b, a)) {
|
|
52
|
+
return 'nested';
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function samePath(a, b) {
|
|
58
|
+
const na = path.resolve(a);
|
|
59
|
+
const nb = path.resolve(b);
|
|
60
|
+
if (process.platform === 'win32') {
|
|
61
|
+
return na.toLowerCase() === nb.toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
return na === nb;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isStrictAncestor(ancestor, descendant) {
|
|
67
|
+
let a = path.resolve(ancestor);
|
|
68
|
+
let d = path.resolve(descendant);
|
|
69
|
+
if (process.platform === 'win32') {
|
|
70
|
+
a = a.toLowerCase();
|
|
71
|
+
d = d.toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
if (samePath(a, d)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const rel = path.relative(a, d);
|
|
77
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function toPosix(abs) {
|
|
81
|
+
return path.resolve(abs).split(path.sep).join('/');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
assertCompatibleOutputDirs,
|
|
86
|
+
classifyOutputDirRelation,
|
|
87
|
+
samePath,
|
|
88
|
+
};
|
|
@@ -5,11 +5,17 @@ const path = require('node:path');
|
|
|
5
5
|
const chokidar = require('chokidar');
|
|
6
6
|
const { loadConfig, CONFIG_FILENAME } = require('./load-config');
|
|
7
7
|
const { resolveProject } = require('./resolve-project');
|
|
8
|
+
const { selectProjectName } = require('./select-project-name');
|
|
9
|
+
const { applyServeCliOverrides } = require('./apply-serve-cli-overrides');
|
|
8
10
|
const { createProjectWatcher } = require('./create-project-watcher');
|
|
9
11
|
const { createStaticServer } = require('./create-static-server');
|
|
10
12
|
const { createLiveReload } = require('./create-live-reload');
|
|
11
13
|
const { formatListenError } = require('../commands/serve-errors');
|
|
12
14
|
const { classifyReload } = require('./classify-reload');
|
|
15
|
+
const {
|
|
16
|
+
buildBrowserOpenUrl,
|
|
17
|
+
openBrowser: defaultOpenBrowser,
|
|
18
|
+
} = require('./open-browser');
|
|
13
19
|
|
|
14
20
|
const DEV_RESTART_KEYS = [
|
|
15
21
|
'outputDir',
|
|
@@ -28,16 +34,24 @@ const DEV_RESTART_KEYS = [
|
|
|
28
34
|
* @param {string|undefined} options.projectName
|
|
29
35
|
* @param {string} [options.commandName]
|
|
30
36
|
* @param {string} [options.usageLine]
|
|
37
|
+
* @param {{ host?: string, port?: string|number, open?: boolean }} [options.cliOverrides]
|
|
31
38
|
* @returns {{ start: Function, close: Function }}
|
|
32
39
|
*/
|
|
33
40
|
function createWatchRuntime(options) {
|
|
34
41
|
const mode = options.mode;
|
|
35
42
|
const workspaceRoot = options.workspaceRoot || process.cwd();
|
|
36
|
-
const projectName = options.projectName;
|
|
37
43
|
const commandName = options.commandName || mode;
|
|
38
44
|
const usageLine =
|
|
39
|
-
options.usageLine || `
|
|
45
|
+
options.usageLine || `jskim ${commandName} [<project>]`;
|
|
46
|
+
const openBrowserFn = options.openBrowserFn || defaultOpenBrowser;
|
|
47
|
+
const cliOverrides = {
|
|
48
|
+
host: options.cliOverrides && options.cliOverrides.host,
|
|
49
|
+
port: options.cliOverrides && options.cliOverrides.port,
|
|
50
|
+
open: Boolean(options.cliOverrides && options.cliOverrides.open),
|
|
51
|
+
};
|
|
40
52
|
|
|
53
|
+
/** @type {string|undefined} */
|
|
54
|
+
let selectedProjectName = options.projectName;
|
|
41
55
|
let currentProject = null;
|
|
42
56
|
let configPath = null;
|
|
43
57
|
let projectWatcher = null;
|
|
@@ -45,6 +59,7 @@ function createWatchRuntime(options) {
|
|
|
45
59
|
let liveReload = null;
|
|
46
60
|
let staticServer = null;
|
|
47
61
|
let liveReloadEnabled = false;
|
|
62
|
+
let browserOpened = false;
|
|
48
63
|
|
|
49
64
|
let stopping = false;
|
|
50
65
|
let started = false;
|
|
@@ -53,15 +68,31 @@ function createWatchRuntime(options) {
|
|
|
53
68
|
let pendingConfigReload = false;
|
|
54
69
|
let sourceBuilding = false;
|
|
55
70
|
|
|
71
|
+
function withCliOverrides(project) {
|
|
72
|
+
return applyServeCliOverrides(project, {
|
|
73
|
+
host: cliOverrides.host,
|
|
74
|
+
port: cliOverrides.port,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
function resolveCurrentProject() {
|
|
57
79
|
const loaded = loadConfig(workspaceRoot);
|
|
58
|
-
const
|
|
80
|
+
const name = selectProjectName({
|
|
59
81
|
config: loaded.config,
|
|
60
|
-
|
|
61
|
-
projectName,
|
|
82
|
+
projectName: selectedProjectName,
|
|
62
83
|
commandName,
|
|
63
84
|
usageLine,
|
|
64
85
|
});
|
|
86
|
+
selectedProjectName = name;
|
|
87
|
+
const project = withCliOverrides(
|
|
88
|
+
resolveProject({
|
|
89
|
+
config: loaded.config,
|
|
90
|
+
workspaceRoot,
|
|
91
|
+
projectName: name,
|
|
92
|
+
commandName,
|
|
93
|
+
usageLine,
|
|
94
|
+
})
|
|
95
|
+
);
|
|
65
96
|
return {
|
|
66
97
|
project,
|
|
67
98
|
configPath: loaded.configPath,
|
|
@@ -118,6 +149,7 @@ function createWatchRuntime(options) {
|
|
|
118
149
|
const host = project.serve.host.trim();
|
|
119
150
|
const port = project.serve.port;
|
|
120
151
|
const outputDisplay = toDisplayPath(project.outputDir, workspaceRoot);
|
|
152
|
+
const browserUrl = buildBrowserOpenUrl({ host, port });
|
|
121
153
|
|
|
122
154
|
liveReload = createLiveReload({
|
|
123
155
|
projectName: project.name,
|
|
@@ -154,6 +186,7 @@ function createWatchRuntime(options) {
|
|
|
154
186
|
host,
|
|
155
187
|
port,
|
|
156
188
|
kind: '開発',
|
|
189
|
+
commandName: 'dev',
|
|
157
190
|
});
|
|
158
191
|
await cleanupDevComponents();
|
|
159
192
|
throw formatted;
|
|
@@ -165,11 +198,30 @@ function createWatchRuntime(options) {
|
|
|
165
198
|
console.log('[JSKim] 開発サーバーを起動しました。');
|
|
166
199
|
console.log(`プロジェクト: ${project.name}`);
|
|
167
200
|
console.log(`ルート: ${outputDisplay}`);
|
|
168
|
-
console.log(`URL: ${
|
|
201
|
+
console.log(`URL: ${browserUrl}`);
|
|
169
202
|
console.log(
|
|
170
203
|
`ライブリロード: ${liveReloadEnabled ? '有効' : '無効'}`
|
|
171
204
|
);
|
|
172
205
|
console.log('終了するには Ctrl+C を押してください。');
|
|
206
|
+
maybeOpenBrowser(browserUrl);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function maybeOpenBrowser(url) {
|
|
211
|
+
if (!cliOverrides.open || browserOpened || stopping) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
browserOpened = true;
|
|
215
|
+
const result = openBrowserFn(url);
|
|
216
|
+
if (!result.ok) {
|
|
217
|
+
const message =
|
|
218
|
+
result.error && result.error.message
|
|
219
|
+
? result.error.message
|
|
220
|
+
: String(result.error || 'unknown');
|
|
221
|
+
console.warn(
|
|
222
|
+
`[JSKim] browserを開けませんでした。手動で次のURLを開いてください: ${url}\n` +
|
|
223
|
+
`原因: ${message}`
|
|
224
|
+
);
|
|
173
225
|
}
|
|
174
226
|
}
|
|
175
227
|
|
|
@@ -314,13 +366,22 @@ function createWatchRuntime(options) {
|
|
|
314
366
|
let candidate;
|
|
315
367
|
try {
|
|
316
368
|
const loaded = loadConfig(workspaceRoot);
|
|
317
|
-
|
|
369
|
+
const name = selectProjectName({
|
|
318
370
|
config: loaded.config,
|
|
319
|
-
|
|
320
|
-
projectName,
|
|
371
|
+
projectName: selectedProjectName,
|
|
321
372
|
commandName,
|
|
322
373
|
usageLine,
|
|
323
374
|
});
|
|
375
|
+
selectedProjectName = name;
|
|
376
|
+
candidate = withCliOverrides(
|
|
377
|
+
resolveProject({
|
|
378
|
+
config: loaded.config,
|
|
379
|
+
workspaceRoot,
|
|
380
|
+
projectName: name,
|
|
381
|
+
commandName,
|
|
382
|
+
usageLine,
|
|
383
|
+
})
|
|
384
|
+
);
|
|
324
385
|
} catch (err) {
|
|
325
386
|
const message = err && err.message ? err.message : String(err);
|
|
326
387
|
console.error('[JSKim] 設定ファイルの再読み込みに失敗しました。');
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* browser で開く URL を組み立てます(listen host と分離)。
|
|
7
|
+
*
|
|
8
|
+
* @param {{ host: string, port: number }} options
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
function buildBrowserOpenUrl({ host, port }) {
|
|
12
|
+
let browserHost = String(host || '').trim();
|
|
13
|
+
if (browserHost === '0.0.0.0') {
|
|
14
|
+
browserHost = '127.0.0.1';
|
|
15
|
+
} else if (browserHost === '::') {
|
|
16
|
+
browserHost = 'localhost';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const portText = String(port);
|
|
20
|
+
if (browserHost.includes(':') && !browserHost.startsWith('[')) {
|
|
21
|
+
return `http://[${browserHost}]:${portText}/`;
|
|
22
|
+
}
|
|
23
|
+
return `http://${browserHost}:${portText}/`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* OS別の open 用 executable / args を返します(shell 不使用)。
|
|
28
|
+
*
|
|
29
|
+
* @param {string} url
|
|
30
|
+
* @param {string} [platform=process.platform]
|
|
31
|
+
* @returns {{ command: string, args: string[] }}
|
|
32
|
+
*/
|
|
33
|
+
function buildOpenBrowserCommand(url, platform = process.platform) {
|
|
34
|
+
if (typeof url !== 'string' || url.trim() === '') {
|
|
35
|
+
throw new Error('[JSKim] browser URLが空です。');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (platform === 'win32') {
|
|
39
|
+
return {
|
|
40
|
+
command: 'rundll32.exe',
|
|
41
|
+
args: ['url.dll,FileProtocolHandler', url],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (platform === 'darwin') {
|
|
46
|
+
return {
|
|
47
|
+
command: 'open',
|
|
48
|
+
args: [url],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
command: 'xdg-open',
|
|
54
|
+
args: [url],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* browser を開きます。失敗しても throw せず結果を返します。
|
|
60
|
+
*
|
|
61
|
+
* @param {string} url
|
|
62
|
+
* @param {object} [options]
|
|
63
|
+
* @param {typeof spawn} [options.spawnFn]
|
|
64
|
+
* @param {string} [options.platform]
|
|
65
|
+
* @returns {{ ok: boolean, error?: Error, command: string, args: string[] }}
|
|
66
|
+
*/
|
|
67
|
+
function openBrowser(url, options = {}) {
|
|
68
|
+
const spawnFn = options.spawnFn || spawn;
|
|
69
|
+
const platform = options.platform || process.platform;
|
|
70
|
+
const { command, args } = buildOpenBrowserCommand(url, platform);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const child = spawnFn(command, args, {
|
|
74
|
+
shell: false,
|
|
75
|
+
stdio: 'ignore',
|
|
76
|
+
detached: true,
|
|
77
|
+
windowsHide: true,
|
|
78
|
+
});
|
|
79
|
+
if (child && typeof child.unref === 'function') {
|
|
80
|
+
child.unref();
|
|
81
|
+
}
|
|
82
|
+
if (child && typeof child.on === 'function') {
|
|
83
|
+
child.on('error', () => {
|
|
84
|
+
// warning は呼び出し側で扱う
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, command, args };
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
92
|
+
command,
|
|
93
|
+
args,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
buildBrowserOpenUrl,
|
|
100
|
+
buildOpenBrowserCommand,
|
|
101
|
+
openBrowser,
|
|
102
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const COMMANDS = new Set(['build', 'watch', 'serve', 'dev']);
|
|
4
|
+
|
|
5
|
+
const COMMAND_OPTIONS = {
|
|
6
|
+
build: new Set(['--all']),
|
|
7
|
+
watch: new Set([]),
|
|
8
|
+
serve: new Set(['--host', '--port']),
|
|
9
|
+
dev: new Set(['--host', '--port', '--open']),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const VALUE_OPTIONS = new Set(['--host', '--port']);
|
|
13
|
+
const BOOLEAN_OPTIONS = new Set(['--all', '--open']);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* jskim の argv(node / script を除く)を解析します。
|
|
17
|
+
*
|
|
18
|
+
* @param {string[]} argv
|
|
19
|
+
* @returns {{
|
|
20
|
+
* kind: 'help'|'version'|'command',
|
|
21
|
+
* command?: string,
|
|
22
|
+
* projectName?: string,
|
|
23
|
+
* options: { all: boolean, open: boolean, host?: string, port?: string }
|
|
24
|
+
* }}
|
|
25
|
+
*/
|
|
26
|
+
function parseJskimArgv(argv) {
|
|
27
|
+
const args = Array.isArray(argv) ? argv.slice() : [];
|
|
28
|
+
|
|
29
|
+
if (args.length === 0) {
|
|
30
|
+
return { kind: 'help', options: emptyOptions() };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const first = args[0];
|
|
34
|
+
if (first === '--help' || first === '-h' || first === 'help') {
|
|
35
|
+
return { kind: 'help', options: emptyOptions() };
|
|
36
|
+
}
|
|
37
|
+
if (first === '--version' || first === '-v') {
|
|
38
|
+
return { kind: 'version', options: emptyOptions() };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!COMMANDS.has(first)) {
|
|
42
|
+
throw new Error(formatUnknownCommand(first));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parsed = parseCommandArgv(first, args.slice(1));
|
|
46
|
+
return {
|
|
47
|
+
kind: 'command',
|
|
48
|
+
command: first,
|
|
49
|
+
projectName: parsed.projectName,
|
|
50
|
+
options: parsed.options,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 特定 command の残 argv を解析します(scripts/*.js 用)。
|
|
56
|
+
*
|
|
57
|
+
* @param {string} command
|
|
58
|
+
* @param {string[]} argv command 名を除いた引数、または scripts の process.argv.slice(2)
|
|
59
|
+
* @returns {{ projectName?: string, options: object }}
|
|
60
|
+
*/
|
|
61
|
+
function parseCommandArgv(command, argv) {
|
|
62
|
+
if (!COMMANDS.has(command)) {
|
|
63
|
+
throw new Error(formatUnknownCommand(command));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const allowed = COMMAND_OPTIONS[command];
|
|
67
|
+
const args = Array.isArray(argv) ? argv.slice() : [];
|
|
68
|
+
const options = emptyOptions();
|
|
69
|
+
/** @type {string[]} */
|
|
70
|
+
const positionals = [];
|
|
71
|
+
/** @type {Set<string>} */
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
75
|
+
const token = args[i];
|
|
76
|
+
|
|
77
|
+
if (token === '--') {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`[JSKim] サポートされていない引数です: --\n` +
|
|
80
|
+
`使用方法: jskim ${command} ...`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (token.startsWith('-')) {
|
|
85
|
+
if (token.includes('=') && token.startsWith('--')) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`[JSKim] この書き方のoptionはサポートしていません: ${token}\n` +
|
|
88
|
+
`例: --port 4000(= は使えません)`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!BOOLEAN_OPTIONS.has(token) && !VALUE_OPTIONS.has(token)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`[JSKim] 不明なoptionです: ${token}\n` +
|
|
95
|
+
formatAllowedOptions(command)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!allowed.has(token)) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`[JSKim] コマンド "${command}" ではoption ${token} を使えません。\n` +
|
|
102
|
+
formatAllowedOptions(command)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (seen.has(token)) {
|
|
107
|
+
throw new Error(`[JSKim] optionが重複しています: ${token}`);
|
|
108
|
+
}
|
|
109
|
+
seen.add(token);
|
|
110
|
+
|
|
111
|
+
if (BOOLEAN_OPTIONS.has(token)) {
|
|
112
|
+
if (token === '--all') {
|
|
113
|
+
options.all = true;
|
|
114
|
+
} else if (token === '--open') {
|
|
115
|
+
options.open = true;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// value option
|
|
121
|
+
const value = args[i + 1];
|
|
122
|
+
if (
|
|
123
|
+
value === undefined ||
|
|
124
|
+
(value.startsWith('-') && isKnownOptionToken(value))
|
|
125
|
+
) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`[JSKim] option ${token} の値がありません。\n` +
|
|
128
|
+
`使用方法: ${token} <value>`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
i += 1;
|
|
132
|
+
if (token === '--host') {
|
|
133
|
+
options.host = value;
|
|
134
|
+
} else if (token === '--port') {
|
|
135
|
+
options.port = value;
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
positionals.push(token);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (positionals.length > 1) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`[JSKim] project名は1つだけ指定してください。\n` +
|
|
146
|
+
`受け取った値: ${positionals.join(', ')}\n` +
|
|
147
|
+
`使用方法: jskim ${command} [<project>]`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (options.all && positionals.length > 0) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`[JSKim] --all と project名は同時に指定できません。\n` +
|
|
154
|
+
`使用方法: jskim build --all\n` +
|
|
155
|
+
`または: jskim build <project>`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (options.all && command !== 'build') {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`[JSKim] コマンド "${command}" ではoption --all を使えません。\n` +
|
|
162
|
+
formatAllowedOptions(command)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
projectName: positionals[0],
|
|
168
|
+
options,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function emptyOptions() {
|
|
173
|
+
return {
|
|
174
|
+
all: false,
|
|
175
|
+
open: false,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isKnownOptionToken(token) {
|
|
180
|
+
return (
|
|
181
|
+
BOOLEAN_OPTIONS.has(token) ||
|
|
182
|
+
VALUE_OPTIONS.has(token) ||
|
|
183
|
+
token === '--help' ||
|
|
184
|
+
token === '-h' ||
|
|
185
|
+
token === '--version' ||
|
|
186
|
+
token === '-v'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function formatAllowedOptions(command) {
|
|
191
|
+
const allowed = [...COMMAND_OPTIONS[command]];
|
|
192
|
+
if (allowed.length === 0) {
|
|
193
|
+
return `コマンド "${command}" で使える追加optionはありません。`;
|
|
194
|
+
}
|
|
195
|
+
return `使えるoption: ${allowed.join(', ')}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatUnknownCommand(command) {
|
|
199
|
+
return [
|
|
200
|
+
`[JSKim] 不明なコマンドです: ${command}`,
|
|
201
|
+
'',
|
|
202
|
+
'使用できるコマンド:',
|
|
203
|
+
' build [<project>]',
|
|
204
|
+
' build --all',
|
|
205
|
+
' watch [<project>]',
|
|
206
|
+
' serve [<project>] [--host <host>] [--port <port>]',
|
|
207
|
+
' dev [<project>] [--host <host>] [--port <port>] [--open]',
|
|
208
|
+
].join('\n');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = {
|
|
212
|
+
parseJskimArgv,
|
|
213
|
+
parseCommandArgv,
|
|
214
|
+
COMMANDS,
|
|
215
|
+
COMMAND_OPTIONS,
|
|
216
|
+
};
|