@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 +27 -11
- package/bin/jskim.js +32 -29
- package/docs/configuration.md +26 -3
- package/docs/create-jskim.md +3 -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/classify-reload.js +131 -0
- package/scripts/lib/create-live-reload.js +422 -32
- package/scripts/lib/create-project-watcher.js +3 -1
- package/scripts/lib/create-watch-runtime.js +109 -16
- 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/lib/stylesheet-reload.js +58 -0
- package/scripts/serve.js +21 -2
- package/scripts/watch.js +13 -2
|
@@ -5,10 +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');
|
|
14
|
+
const { classifyReload } = require('./classify-reload');
|
|
15
|
+
const {
|
|
16
|
+
buildBrowserOpenUrl,
|
|
17
|
+
openBrowser: defaultOpenBrowser,
|
|
18
|
+
} = require('./open-browser');
|
|
12
19
|
|
|
13
20
|
const DEV_RESTART_KEYS = [
|
|
14
21
|
'outputDir',
|
|
@@ -27,16 +34,24 @@ const DEV_RESTART_KEYS = [
|
|
|
27
34
|
* @param {string|undefined} options.projectName
|
|
28
35
|
* @param {string} [options.commandName]
|
|
29
36
|
* @param {string} [options.usageLine]
|
|
37
|
+
* @param {{ host?: string, port?: string|number, open?: boolean }} [options.cliOverrides]
|
|
30
38
|
* @returns {{ start: Function, close: Function }}
|
|
31
39
|
*/
|
|
32
40
|
function createWatchRuntime(options) {
|
|
33
41
|
const mode = options.mode;
|
|
34
42
|
const workspaceRoot = options.workspaceRoot || process.cwd();
|
|
35
|
-
const projectName = options.projectName;
|
|
36
43
|
const commandName = options.commandName || mode;
|
|
37
44
|
const usageLine =
|
|
38
|
-
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
|
+
};
|
|
39
52
|
|
|
53
|
+
/** @type {string|undefined} */
|
|
54
|
+
let selectedProjectName = options.projectName;
|
|
40
55
|
let currentProject = null;
|
|
41
56
|
let configPath = null;
|
|
42
57
|
let projectWatcher = null;
|
|
@@ -44,6 +59,7 @@ function createWatchRuntime(options) {
|
|
|
44
59
|
let liveReload = null;
|
|
45
60
|
let staticServer = null;
|
|
46
61
|
let liveReloadEnabled = false;
|
|
62
|
+
let browserOpened = false;
|
|
47
63
|
|
|
48
64
|
let stopping = false;
|
|
49
65
|
let started = false;
|
|
@@ -52,15 +68,31 @@ function createWatchRuntime(options) {
|
|
|
52
68
|
let pendingConfigReload = false;
|
|
53
69
|
let sourceBuilding = false;
|
|
54
70
|
|
|
71
|
+
function withCliOverrides(project) {
|
|
72
|
+
return applyServeCliOverrides(project, {
|
|
73
|
+
host: cliOverrides.host,
|
|
74
|
+
port: cliOverrides.port,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
function resolveCurrentProject() {
|
|
56
79
|
const loaded = loadConfig(workspaceRoot);
|
|
57
|
-
const
|
|
80
|
+
const name = selectProjectName({
|
|
58
81
|
config: loaded.config,
|
|
59
|
-
|
|
60
|
-
projectName,
|
|
82
|
+
projectName: selectedProjectName,
|
|
61
83
|
commandName,
|
|
62
84
|
usageLine,
|
|
63
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
|
+
);
|
|
64
96
|
return {
|
|
65
97
|
project,
|
|
66
98
|
configPath: loaded.configPath,
|
|
@@ -77,13 +109,15 @@ function createWatchRuntime(options) {
|
|
|
77
109
|
currentProject = resolved.project;
|
|
78
110
|
configPath = resolved.configPath;
|
|
79
111
|
|
|
112
|
+
// ready ログより先に config 監視を開始する。
|
|
113
|
+
// そうしないと「監視しています」直後の config 変更を見逃すことがある。
|
|
114
|
+
beginConfigWatching();
|
|
115
|
+
|
|
80
116
|
if (mode === 'dev') {
|
|
81
117
|
await startDevSession(currentProject, { initial: true });
|
|
82
118
|
} else {
|
|
83
119
|
await startWatchSession(currentProject, { initial: true });
|
|
84
120
|
}
|
|
85
|
-
|
|
86
|
-
beginConfigWatching();
|
|
87
121
|
}
|
|
88
122
|
|
|
89
123
|
async function startWatchSession(project, { initial }) {
|
|
@@ -115,6 +149,7 @@ function createWatchRuntime(options) {
|
|
|
115
149
|
const host = project.serve.host.trim();
|
|
116
150
|
const port = project.serve.port;
|
|
117
151
|
const outputDisplay = toDisplayPath(project.outputDir, workspaceRoot);
|
|
152
|
+
const browserUrl = buildBrowserOpenUrl({ host, port });
|
|
118
153
|
|
|
119
154
|
liveReload = createLiveReload({
|
|
120
155
|
projectName: project.name,
|
|
@@ -151,6 +186,7 @@ function createWatchRuntime(options) {
|
|
|
151
186
|
host,
|
|
152
187
|
port,
|
|
153
188
|
kind: '開発',
|
|
189
|
+
commandName: 'dev',
|
|
154
190
|
});
|
|
155
191
|
await cleanupDevComponents();
|
|
156
192
|
throw formatted;
|
|
@@ -162,11 +198,30 @@ function createWatchRuntime(options) {
|
|
|
162
198
|
console.log('[JSKim] 開発サーバーを起動しました。');
|
|
163
199
|
console.log(`プロジェクト: ${project.name}`);
|
|
164
200
|
console.log(`ルート: ${outputDisplay}`);
|
|
165
|
-
console.log(`URL: ${
|
|
201
|
+
console.log(`URL: ${browserUrl}`);
|
|
166
202
|
console.log(
|
|
167
203
|
`ライブリロード: ${liveReloadEnabled ? '有効' : '無効'}`
|
|
168
204
|
);
|
|
169
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
|
+
);
|
|
170
225
|
}
|
|
171
226
|
}
|
|
172
227
|
|
|
@@ -185,10 +240,28 @@ function createWatchRuntime(options) {
|
|
|
185
240
|
}
|
|
186
241
|
|
|
187
242
|
function wireDevSourceReload(watcher) {
|
|
188
|
-
watcher.on('build:success', ({ initial }) => {
|
|
189
|
-
if (
|
|
190
|
-
|
|
243
|
+
watcher.on('build:success', ({ initial, events }) => {
|
|
244
|
+
if (initial || !liveReloadEnabled || !liveReload) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// 未解決の config error がある間は source 成功で overlay/reload しない
|
|
248
|
+
if (liveReload.hasConfigError()) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const kind = classifyReload({
|
|
252
|
+
events,
|
|
253
|
+
sourceDir: currentProject && currentProject.sourceDir,
|
|
254
|
+
templates: currentProject && currentProject.templates,
|
|
255
|
+
});
|
|
256
|
+
liveReload.notifySourceBuildSuccess(kind);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
watcher.on('build:failure', ({ error }) => {
|
|
260
|
+
if (!liveReloadEnabled || !liveReload) {
|
|
261
|
+
return;
|
|
191
262
|
}
|
|
263
|
+
// Error.message は完成済みの診断文字列を再利用する
|
|
264
|
+
liveReload.broadcastBuildError(error);
|
|
192
265
|
});
|
|
193
266
|
}
|
|
194
267
|
|
|
@@ -293,21 +366,38 @@ function createWatchRuntime(options) {
|
|
|
293
366
|
let candidate;
|
|
294
367
|
try {
|
|
295
368
|
const loaded = loadConfig(workspaceRoot);
|
|
296
|
-
|
|
369
|
+
const name = selectProjectName({
|
|
297
370
|
config: loaded.config,
|
|
298
|
-
|
|
299
|
-
projectName,
|
|
371
|
+
projectName: selectedProjectName,
|
|
300
372
|
commandName,
|
|
301
373
|
usageLine,
|
|
302
374
|
});
|
|
375
|
+
selectedProjectName = name;
|
|
376
|
+
candidate = withCliOverrides(
|
|
377
|
+
resolveProject({
|
|
378
|
+
config: loaded.config,
|
|
379
|
+
workspaceRoot,
|
|
380
|
+
projectName: name,
|
|
381
|
+
commandName,
|
|
382
|
+
usageLine,
|
|
383
|
+
})
|
|
384
|
+
);
|
|
303
385
|
} catch (err) {
|
|
304
386
|
const message = err && err.message ? err.message : String(err);
|
|
305
387
|
console.error('[JSKim] 設定ファイルの再読み込みに失敗しました。');
|
|
306
388
|
console.error(message);
|
|
307
389
|
console.error('[JSKim] 以前の正常な設定を継続します。');
|
|
390
|
+
if (mode === 'dev' && liveReloadEnabled && liveReload) {
|
|
391
|
+
liveReload.broadcastConfigError(message);
|
|
392
|
+
}
|
|
308
393
|
return;
|
|
309
394
|
}
|
|
310
395
|
|
|
396
|
+
// candidate は validation 済み。以降の失敗は build error として扱う
|
|
397
|
+
if (mode === 'dev' && liveReloadEnabled && liveReload) {
|
|
398
|
+
liveReload.clearConfigError();
|
|
399
|
+
}
|
|
400
|
+
|
|
311
401
|
if (mode === 'dev') {
|
|
312
402
|
const restartKeys = getRestartRequiredChanges(currentProject, candidate);
|
|
313
403
|
if (restartKeys.length > 0) {
|
|
@@ -348,6 +438,9 @@ function createWatchRuntime(options) {
|
|
|
348
438
|
console.error('[JSKim] 設定の再読み込み後に監視の更新に失敗しました。');
|
|
349
439
|
console.error(message);
|
|
350
440
|
console.error('[JSKim] 以前の正常な設定を継続します。');
|
|
441
|
+
if (mode === 'dev' && liveReloadEnabled && liveReload) {
|
|
442
|
+
liveReload.broadcastConfigError(message);
|
|
443
|
+
}
|
|
351
444
|
return;
|
|
352
445
|
}
|
|
353
446
|
|
|
@@ -388,9 +481,9 @@ function createWatchRuntime(options) {
|
|
|
388
481
|
hooks.onInitialBuildSuccess();
|
|
389
482
|
}
|
|
390
483
|
};
|
|
391
|
-
const onFailure = ({ initial }) => {
|
|
484
|
+
const onFailure = ({ initial, error }) => {
|
|
392
485
|
if (initial && hooks.onInitialBuildFailure) {
|
|
393
|
-
hooks.onInitialBuildFailure();
|
|
486
|
+
hooks.onInitialBuildFailure(error);
|
|
394
487
|
}
|
|
395
488
|
};
|
|
396
489
|
projectWatcher.on('build:success', onSuccess);
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* config.projects のキーを定義順で返します。
|
|
5
|
+
* @param {object} config
|
|
6
|
+
* @returns {string[]}
|
|
7
|
+
*/
|
|
8
|
+
function listProjectNames(config) {
|
|
9
|
+
const projects = config && config.projects;
|
|
10
|
+
if (!projects || typeof projects !== 'object') {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
return Object.keys(projects);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* positional project が無い場合の自動選択、または明示名を返します。
|
|
18
|
+
*
|
|
19
|
+
* @param {object} options
|
|
20
|
+
* @param {object} options.config
|
|
21
|
+
* @param {string|undefined} options.projectName
|
|
22
|
+
* @param {string} [options.commandName]
|
|
23
|
+
* @param {string} [options.usageLine]
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function selectProjectName({
|
|
27
|
+
config,
|
|
28
|
+
projectName,
|
|
29
|
+
commandName = 'build',
|
|
30
|
+
usageLine,
|
|
31
|
+
}) {
|
|
32
|
+
const names = listProjectNames(config);
|
|
33
|
+
const usage =
|
|
34
|
+
usageLine || `jskim ${commandName} [<project>]`;
|
|
35
|
+
|
|
36
|
+
if (projectName && String(projectName).trim() !== '') {
|
|
37
|
+
return String(projectName).trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (names.length === 0) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`[JSKim] 設定にprojectがありません。\n` +
|
|
43
|
+
`jskim.config.js の projects に1件以上定義してください。\n` +
|
|
44
|
+
`使用方法: ${usage}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (names.length === 1) {
|
|
49
|
+
return names[0];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const list = names.map((name) => `- ${name}`).join('\n');
|
|
53
|
+
throw new Error(
|
|
54
|
+
`[JSKim] projectを指定してください。\n` +
|
|
55
|
+
`使用方法: ${usage}\n\n` +
|
|
56
|
+
`利用可能なproject:\n` +
|
|
57
|
+
list
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
listProjectNames,
|
|
63
|
+
selectProjectName,
|
|
64
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CSS_CACHE_PARAM = '_jskim';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* same-origin の stylesheet URL かどうかを判定します。
|
|
7
|
+
* browser runtime へ Function#toString で埋め込むため、外部クロージャに依存しません。
|
|
8
|
+
*
|
|
9
|
+
* @param {string} href
|
|
10
|
+
* @param {string} pageOrigin location.origin または location.href
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
function isSameOriginStylesheetHref(href, pageOrigin) {
|
|
14
|
+
if (!href || typeof href !== 'string') {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const trimmed = href.trim();
|
|
18
|
+
if (!trimmed || trimmed.startsWith('data:')) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const resolved = new URL(trimmed, pageOrigin);
|
|
24
|
+
const origin = new URL(pageOrigin);
|
|
25
|
+
return resolved.origin === origin.origin;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* cache-busting query を付与または更新します。hash と他 query は維持します。
|
|
33
|
+
* browser runtime へ Function#toString で埋め込むため、パラメータ名はリテラルです。
|
|
34
|
+
*
|
|
35
|
+
* @param {string} href
|
|
36
|
+
* @param {string} pageOrigin
|
|
37
|
+
* @param {string|number} token
|
|
38
|
+
* @returns {string|null}
|
|
39
|
+
*/
|
|
40
|
+
function appendCacheBustParam(href, pageOrigin, token) {
|
|
41
|
+
if (!isSameOriginStylesheetHref(href, pageOrigin)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const url = new URL(href, pageOrigin);
|
|
47
|
+
url.searchParams.set('_jskim', String(token));
|
|
48
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
CSS_CACHE_PARAM,
|
|
56
|
+
isSameOriginStylesheetHref,
|
|
57
|
+
appendCacheBustParam,
|
|
58
|
+
};
|