@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.
@@ -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 || `npm run ${commandName} -- <project-name>`;
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 project = resolveProject({
80
+ const name = selectProjectName({
58
81
  config: loaded.config,
59
- workspaceRoot,
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: ${staticServer.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 (!initial && liveReloadEnabled && liveReload) {
190
- liveReload.broadcastReload();
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
- candidate = resolveProject({
369
+ const name = selectProjectName({
297
370
  config: loaded.config,
298
- workspaceRoot,
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
+ };