@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.
@@ -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 || 'npm run watch -- <project-name>';
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: process.argv[2],
17
+ projectName: parsed.projectName,
7
18
  workspaceRoot: process.cwd(),
8
- usageLine: 'npm run dev -- <project-name>',
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
+ };
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const fs = require('node:fs');
5
+
6
+ /**
7
+ * rebuild 単位の watcher events から browser 更新方法を分類します。
8
+ * 不確実な場合は必ず full reload にします。
9
+ *
10
+ * @param {object} options
11
+ * @param {Array<{ event?: string, absolutePath?: string, file?: string }>} options.events
12
+ * @param {string} options.sourceDir
13
+ * @param {string[]} [options.templates]
14
+ * @returns {'css'|'reload'}
15
+ */
16
+ function classifyReload(options) {
17
+ const events = Array.isArray(options.events) ? options.events : [];
18
+ if (events.length === 0) {
19
+ return 'reload';
20
+ }
21
+
22
+ const templateRoots = resolveTemplateRoots(
23
+ options.sourceDir,
24
+ options.templates
25
+ );
26
+
27
+ for (const item of events) {
28
+ if (!item || typeof item !== 'object') {
29
+ return 'reload';
30
+ }
31
+
32
+ const eventName = item.event;
33
+ if (eventName !== 'change') {
34
+ return 'reload';
35
+ }
36
+
37
+ const absolutePath = resolveEventAbsolutePath(item);
38
+ if (!absolutePath) {
39
+ return 'reload';
40
+ }
41
+
42
+ if (isInsideAny(templateRoots, absolutePath)) {
43
+ return 'reload';
44
+ }
45
+
46
+ if (!isCssSourcePath(absolutePath)) {
47
+ return 'reload';
48
+ }
49
+ }
50
+
51
+ return 'css';
52
+ }
53
+
54
+ /**
55
+ * @param {{ absolutePath?: string, file?: string }} item
56
+ * @returns {string|null}
57
+ */
58
+ function resolveEventAbsolutePath(item) {
59
+ if (item.absolutePath && typeof item.absolutePath === 'string') {
60
+ const abs = path.resolve(item.absolutePath);
61
+ if (path.isAbsolute(abs)) {
62
+ return abs;
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * 末尾が .css または .css.njk のときだけ true(大文字小文字は無視)。
70
+ * @param {string} absolutePath
71
+ * @returns {boolean}
72
+ */
73
+ function isCssSourcePath(absolutePath) {
74
+ const normalized = absolutePath.split(path.sep).join('/').toLowerCase();
75
+ return normalized.endsWith('.css.njk') || normalized.endsWith('.css');
76
+ }
77
+
78
+ function resolveTemplateRoots(sourceDir, templates) {
79
+ const roots = [];
80
+ if (!sourceDir) {
81
+ return roots;
82
+ }
83
+ for (const rel of Array.isArray(templates) ? templates : []) {
84
+ if (!rel) {
85
+ continue;
86
+ }
87
+ const abs = path.resolve(sourceDir, rel);
88
+ try {
89
+ if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
90
+ roots.push(abs);
91
+ }
92
+ } catch {
93
+ // 存在確認失敗は root として扱わない
94
+ }
95
+ }
96
+ return roots;
97
+ }
98
+
99
+ function isInsideAny(roots, candidate) {
100
+ for (const root of roots) {
101
+ if (isInsideOrSame(root, candidate)) {
102
+ return true;
103
+ }
104
+ }
105
+ return false;
106
+ }
107
+
108
+ function isInsideOrSame(parent, child) {
109
+ const p = path.resolve(parent);
110
+ const c = path.resolve(child);
111
+ if (samePath(p, c)) {
112
+ return true;
113
+ }
114
+ const rel = path.relative(p, c);
115
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
116
+ }
117
+
118
+ function samePath(a, b) {
119
+ const na = path.resolve(a);
120
+ const nb = path.resolve(b);
121
+ if (process.platform === 'win32') {
122
+ return na.toLowerCase() === nb.toLowerCase();
123
+ }
124
+ return na === nb;
125
+ }
126
+
127
+ module.exports = {
128
+ classifyReload,
129
+ isCssSourcePath,
130
+ resolveTemplateRoots,
131
+ };