@ywal123456/jskim 0.1.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.
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ const { loadConfig } = require('../lib/load-config');
4
+ const { resolveProject } = require('../lib/resolve-project');
5
+ const { createProjectWatcher } = require('../lib/create-project-watcher');
6
+ const { createStaticServer } = require('../lib/create-static-server');
7
+ const { createLiveReload } = require('../lib/create-live-reload');
8
+ const { registerShutdown } = require('./register-shutdown');
9
+ const { toDisplayPath } = require('./path-display');
10
+ const { formatListenError } = require('./serve-errors');
11
+
12
+ /**
13
+ * dev コマンドを実行します。
14
+ * @param {object} options
15
+ * @param {string|undefined} options.projectName
16
+ * @param {string} [options.workspaceRoot]
17
+ * @param {string} [options.usageLine]
18
+ * @returns {Promise<void>}
19
+ */
20
+ async function runDevCommand(options = {}) {
21
+ const workspaceRoot = options.workspaceRoot || process.cwd();
22
+ const usageLine =
23
+ options.usageLine || 'npm run dev -- <project-name>';
24
+
25
+ const { config } = loadConfig(workspaceRoot);
26
+ const project = resolveProject({
27
+ config,
28
+ workspaceRoot,
29
+ projectName: options.projectName,
30
+ commandName: 'dev',
31
+ usageLine,
32
+ });
33
+
34
+ const host = project.serve.host.trim();
35
+ const port = project.serve.port;
36
+ const liveReloadEnabled = project.dev.liveReload;
37
+ const outputDisplay = toDisplayPath(project.outputDir, workspaceRoot);
38
+
39
+ const liveReload = createLiveReload({
40
+ projectName: project.name,
41
+ enabled: liveReloadEnabled,
42
+ });
43
+
44
+ const staticServer = createStaticServer({
45
+ rootDir: project.outputDir,
46
+ host,
47
+ port,
48
+ projectName: project.name,
49
+ handleInternalRequest: (req, res, meta) =>
50
+ liveReload.handleRequest(req, res, meta),
51
+ transformHtml: liveReloadEnabled
52
+ ? (html) => liveReload.injectHtml(html)
53
+ : undefined,
54
+ });
55
+
56
+ const projectWatcher = createProjectWatcher(project, {
57
+ runInitialBuild: true,
58
+ logChanges: true,
59
+ });
60
+
61
+ // 成功した再ビルドのあとだけ reload(初回はクライアント未接続のため不要)
62
+ projectWatcher.on('build:success', ({ initial }) => {
63
+ if (!initial && liveReloadEnabled) {
64
+ liveReload.broadcastReload();
65
+ }
66
+ });
67
+
68
+ let stopping = false;
69
+ let watcherStarted = false;
70
+
71
+ async function shutdown() {
72
+ if (stopping) {
73
+ return;
74
+ }
75
+ stopping = true;
76
+
77
+ try {
78
+ if (watcherStarted) {
79
+ await projectWatcher.close();
80
+ }
81
+ } catch {
82
+ // 終了時エラーは無視
83
+ }
84
+
85
+ try {
86
+ liveReload.close();
87
+ } catch {
88
+ // 終了時エラーは無視
89
+ }
90
+
91
+ try {
92
+ await staticServer.stop();
93
+ } catch {
94
+ // 終了時エラーは無視
95
+ }
96
+
97
+ console.log('[JSKim] 開発サーバーを停止しました。');
98
+ process.exit(0);
99
+ }
100
+
101
+ async function failStartup(err) {
102
+ const message = err && err.message ? err.message : String(err);
103
+ console.error(message);
104
+
105
+ try {
106
+ if (watcherStarted) {
107
+ await projectWatcher.close();
108
+ }
109
+ } catch {
110
+ // ignore
111
+ }
112
+
113
+ try {
114
+ liveReload.close();
115
+ } catch {
116
+ // ignore
117
+ }
118
+
119
+ try {
120
+ await staticServer.stop();
121
+ } catch {
122
+ // ignore
123
+ }
124
+
125
+ // cleanup 完了後に終了する。
126
+ // IPC 接続があると exitCode だけではプロセスが残るため明示終了する。
127
+ process.exit(1);
128
+ }
129
+
130
+ registerShutdown(shutdown);
131
+
132
+ // 初回 build → server → watcher
133
+ await projectWatcher.start({ watchFiles: false });
134
+ watcherStarted = true;
135
+
136
+ try {
137
+ await staticServer.start();
138
+ } catch (err) {
139
+ await failStartup(
140
+ formatListenError(err, {
141
+ projectName: project.name,
142
+ host,
143
+ port,
144
+ kind: '開発',
145
+ })
146
+ );
147
+ return;
148
+ }
149
+
150
+ await projectWatcher.startWatching();
151
+
152
+ console.log('[JSKim] 開発サーバーを起動しました。');
153
+ console.log(`プロジェクト: ${project.name}`);
154
+ console.log(`ルート: ${outputDisplay}`);
155
+ console.log(`URL: ${staticServer.url}`);
156
+ console.log(
157
+ `ライブリロード: ${liveReloadEnabled ? '有効' : '無効'}`
158
+ );
159
+ console.log('終了するには Ctrl+C を押してください。');
160
+ }
161
+
162
+ module.exports = {
163
+ runDevCommand,
164
+ };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CLI ヘルプ文言を返します。
5
+ * @returns {string}
6
+ */
7
+ function getHelpText() {
8
+ return [
9
+ 'JSKim - Nunjucksを使用した静的HTMLビルド環境',
10
+ '',
11
+ '使用方法:',
12
+ ' jskim <command> <project>',
13
+ '',
14
+ 'コマンド:',
15
+ ' build <project> 静的ファイルをビルドします。',
16
+ ' watch <project> ファイルの変更を監視して再ビルドします。',
17
+ ' serve <project> ビルド済みのファイルを配信します。',
18
+ ' dev <project> ビルド・監視・サーバー・ライブリロードを起動します。',
19
+ '',
20
+ 'オプション:',
21
+ ' -h, --help ヘルプを表示します。',
22
+ ' -v, --version バージョンを表示します。',
23
+ ].join('\n');
24
+ }
25
+
26
+ /**
27
+ * 不明なコマンド時の案内を返します。
28
+ * @param {string} command
29
+ * @returns {string}
30
+ */
31
+ function getUnknownCommandText(command) {
32
+ return [
33
+ `[JSKim] 不明なコマンドです: ${command}`,
34
+ '',
35
+ '使用できるコマンド:',
36
+ ' build <project>',
37
+ ' watch <project>',
38
+ ' serve <project>',
39
+ ' dev <project>',
40
+ ].join('\n');
41
+ }
42
+
43
+ module.exports = {
44
+ getHelpText,
45
+ getUnknownCommandText,
46
+ };
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ /**
6
+ * ワークスペース相対の表示用パスへ変換します。
7
+ * @param {string} abs
8
+ * @param {string} workspaceRoot
9
+ * @returns {string}
10
+ */
11
+ function toDisplayPath(abs, workspaceRoot) {
12
+ const rel = path.relative(workspaceRoot, abs);
13
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
14
+ return abs.split(path.sep).join('/');
15
+ }
16
+ return rel.split(path.sep).join('/');
17
+ }
18
+
19
+ module.exports = {
20
+ toDisplayPath,
21
+ };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CLI の SIGINT/SIGTERM と内部検証用 IPC を登録します。
5
+ * @param {() => void|Promise<void>} shutdown
6
+ */
7
+ function registerShutdown(shutdown) {
8
+ process.on('SIGINT', () => {
9
+ shutdown();
10
+ });
11
+ process.on('SIGTERM', () => {
12
+ shutdown();
13
+ });
14
+
15
+ if (process.platform === 'win32' && process.stdin.isTTY) {
16
+ const readline = require('node:readline');
17
+ readline.createInterface({ input: process.stdin }).on('SIGINT', () => {
18
+ shutdown();
19
+ });
20
+ }
21
+
22
+ // 内部検証用 IPC(公開機能ではない。一般利用は Ctrl+C)
23
+ process.on('message', (msg) => {
24
+ if (msg === 'jskim:stop') {
25
+ shutdown();
26
+ }
27
+ });
28
+ }
29
+
30
+ module.exports = {
31
+ registerShutdown,
32
+ };
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const { loadConfig } = require('../lib/load-config');
4
+ const { resolveProject } = require('../lib/resolve-project');
5
+ const { createStaticServer } = require('../lib/create-static-server');
6
+ const { registerShutdown } = require('./register-shutdown');
7
+ const { toDisplayPath } = require('./path-display');
8
+ const { assertOutputDirReady, formatListenError } = require('./serve-errors');
9
+
10
+ /**
11
+ * serve コマンドを実行します。
12
+ * @param {object} options
13
+ * @param {string|undefined} options.projectName
14
+ * @param {string} [options.workspaceRoot]
15
+ * @param {string} [options.usageLine]
16
+ * @param {string} [options.buildHint]
17
+ * @returns {Promise<void>}
18
+ */
19
+ async function runServeCommand(options = {}) {
20
+ const workspaceRoot = options.workspaceRoot || process.cwd();
21
+ const usageLine =
22
+ options.usageLine || 'npm run serve -- <project-name>';
23
+
24
+ const { config } = loadConfig(workspaceRoot);
25
+ const project = resolveProject({
26
+ config,
27
+ workspaceRoot,
28
+ projectName: options.projectName,
29
+ commandName: 'serve',
30
+ usageLine,
31
+ });
32
+
33
+ const buildHint =
34
+ options.buildHint || `npm run build -- ${project.name}`;
35
+ assertOutputDirReady(project, { buildHint });
36
+
37
+ const host = project.serve.host.trim();
38
+ const port = project.serve.port;
39
+ const outputDisplay = toDisplayPath(project.outputDir, workspaceRoot);
40
+
41
+ const staticServer = createStaticServer({
42
+ rootDir: project.outputDir,
43
+ host,
44
+ port,
45
+ projectName: project.name,
46
+ });
47
+
48
+ try {
49
+ await staticServer.start();
50
+ } catch (err) {
51
+ throw formatListenError(err, {
52
+ projectName: project.name,
53
+ host,
54
+ port,
55
+ kind: '静的',
56
+ });
57
+ }
58
+
59
+ console.log('[JSKim] 静的サーバーを起動しました。');
60
+ console.log(`プロジェクト: ${project.name}`);
61
+ console.log(`ルート: ${outputDisplay}`);
62
+ console.log(`URL: ${staticServer.url}`);
63
+ console.log('終了するには Ctrl+C を押してください。');
64
+
65
+ let stopping = false;
66
+
67
+ async function shutdown() {
68
+ if (stopping) {
69
+ return;
70
+ }
71
+ stopping = true;
72
+
73
+ try {
74
+ await staticServer.stop();
75
+ } catch {
76
+ // 終了時の close エラーは無視
77
+ }
78
+
79
+ console.log('[JSKim] 静的サーバーを停止しました。');
80
+ process.exit(0);
81
+ }
82
+
83
+ registerShutdown(shutdown);
84
+ }
85
+
86
+ module.exports = {
87
+ runServeCommand,
88
+ };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+
5
+ /**
6
+ * serve / dev 開始前に outputDir の存在を確認します。
7
+ * @param {object} project
8
+ * @param {object} [options]
9
+ * @param {string} [options.buildHint]
10
+ */
11
+ function assertOutputDirReady(project, options = {}) {
12
+ const { name, outputDir, outputDirConfig } = project;
13
+ const buildHint = options.buildHint || `npm run build -- ${name}`;
14
+
15
+ if (!outputDir || String(outputDir).trim() === '') {
16
+ throw new Error(
17
+ `[JSKim] ビルド出力が見つかりません。\n` +
18
+ `プロジェクト: ${name}\n` +
19
+ `出力先: ${outputDirConfig || '(未設定)'}\n\n` +
20
+ `先にビルドを実行してください:\n` +
21
+ buildHint
22
+ );
23
+ }
24
+
25
+ if (!fs.existsSync(outputDir)) {
26
+ throw new Error(
27
+ `[JSKim] ビルド出力が見つかりません。\n` +
28
+ `プロジェクト: ${name}\n` +
29
+ `出力先: ${outputDirConfig}\n\n` +
30
+ `先にビルドを実行してください:\n` +
31
+ buildHint
32
+ );
33
+ }
34
+
35
+ if (!fs.statSync(outputDir).isDirectory()) {
36
+ throw new Error(
37
+ `[JSKim] ビルド出力が見つかりません。\n` +
38
+ `プロジェクト: ${name}\n` +
39
+ `出力先: ${outputDirConfig}\n` +
40
+ `原因: outputDir はディレクトリではありません。\n\n` +
41
+ `先にビルドを実行してください:\n` +
42
+ buildHint
43
+ );
44
+ }
45
+ }
46
+
47
+ function formatListenError(err, { projectName, host, port, kind = '静的' }) {
48
+ const code = err && err.code;
49
+
50
+ if (code === 'EADDRINUSE') {
51
+ return new Error(
52
+ `[JSKim] ポート ${port} はすでに使用されています。\n` +
53
+ `プロジェクト: ${projectName}\n` +
54
+ `ホスト: ${host}\n\n` +
55
+ `jskim.config.js の serve.port を変更してください。`
56
+ );
57
+ }
58
+
59
+ if (code === 'EACCES') {
60
+ return new Error(
61
+ `[JSKim] ポート ${port} へのバインド権限がありません。\n` +
62
+ `プロジェクト: ${projectName}\n` +
63
+ `ホスト: ${host}\n\n` +
64
+ `別の serve.port を指定するか、権限を確認してください。`
65
+ );
66
+ }
67
+
68
+ const message = err && err.message ? err.message : String(err);
69
+ const label = kind === '開発' ? '開発サーバー' : '静的サーバー';
70
+ return new Error(
71
+ `[JSKim] ${label}の起動に失敗しました。\n` +
72
+ `プロジェクト: ${projectName}\n` +
73
+ `ホスト: ${host}\n` +
74
+ `ポート: ${port}\n` +
75
+ `原因: ${message}`
76
+ );
77
+ }
78
+
79
+ module.exports = {
80
+ assertOutputDirReady,
81
+ formatListenError,
82
+ };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const { loadConfig } = require('../lib/load-config');
4
+ const { resolveProject } = require('../lib/resolve-project');
5
+ const { createProjectWatcher } = require('../lib/create-project-watcher');
6
+ const { registerShutdown } = require('./register-shutdown');
7
+
8
+ /**
9
+ * watch コマンドを実行します。
10
+ * @param {object} options
11
+ * @param {string|undefined} options.projectName
12
+ * @param {string} [options.workspaceRoot]
13
+ * @param {string} [options.usageLine]
14
+ * @returns {Promise<void>}
15
+ */
16
+ async function runWatchCommand(options = {}) {
17
+ const workspaceRoot = options.workspaceRoot || process.cwd();
18
+ const usageLine =
19
+ options.usageLine || 'npm run watch -- <project-name>';
20
+
21
+ const { config } = loadConfig(workspaceRoot);
22
+ const project = resolveProject({
23
+ config,
24
+ workspaceRoot,
25
+ projectName: options.projectName,
26
+ commandName: 'watch',
27
+ usageLine,
28
+ });
29
+
30
+ const projectWatcher = createProjectWatcher(project, {
31
+ runInitialBuild: true,
32
+ logChanges: true,
33
+ });
34
+
35
+ projectWatcher.on('ready', ({ displayPaths, debounceMs }) => {
36
+ console.log(`[JSKim] プロジェクトを監視しています: ${project.name}`);
37
+ console.log('パス:');
38
+ for (const display of displayPaths) {
39
+ console.log(`- ${display}`);
40
+ }
41
+ console.log('');
42
+ console.log(`Debounce: ${debounceMs}ms`);
43
+ console.log('終了するには Ctrl+C を押してください。');
44
+ });
45
+
46
+ await projectWatcher.start();
47
+
48
+ let stopping = false;
49
+
50
+ async function shutdown() {
51
+ if (stopping) {
52
+ return;
53
+ }
54
+ stopping = true;
55
+
56
+ try {
57
+ await projectWatcher.close();
58
+ } catch {
59
+ // 終了時エラーは無視
60
+ }
61
+
62
+ console.log('[JSKim] ウォッチを停止しました。');
63
+ process.exit(0);
64
+ }
65
+
66
+ registerShutdown(shutdown);
67
+ }
68
+
69
+ module.exports = {
70
+ runWatchCommand,
71
+ };
package/scripts/dev.js ADDED
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ const { runDevCommand } = require('./commands/dev-command');
4
+
5
+ runDevCommand({
6
+ projectName: process.argv[2],
7
+ workspaceRoot: process.cwd(),
8
+ usageLine: 'npm run dev -- <project-name>',
9
+ }).catch((err) => {
10
+ const message = err && err.message ? err.message : String(err);
11
+ console.error(message);
12
+ process.exitCode = 1;
13
+ });
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { loadConfig } = require('./load-config');
5
+ const { resolveProject } = require('./resolve-project');
6
+ const { createNunjucksEnv } = require('./create-nunjucks-env');
7
+ const { cleanOutput } = require('./clean-output');
8
+ const { renderPages } = require('./render-pages');
9
+ const { copyFiles } = require('./copy-files');
10
+
11
+ /**
12
+ * 設定を読み込み、プロジェクトを解決してフルビルドを実行します。
13
+ *
14
+ * @param {string|undefined} projectName
15
+ * @param {object} [options]
16
+ * @param {string} [options.workspaceRoot]
17
+ * @param {string} [options.commandName='build']
18
+ * @param {string} [options.usageLine]
19
+ * @param {string} [options.logTitle='ビルドが完了しました']
20
+ * @param {boolean} [options.includeOutput=true]
21
+ * @param {boolean} [options.log=true]
22
+ * @returns {Promise<object>}
23
+ */
24
+ async function buildProject(projectName, options = {}) {
25
+ const workspaceRoot =
26
+ options.workspaceRoot || process.cwd();
27
+ const commandName = options.commandName || 'build';
28
+
29
+ const { config } = loadConfig(workspaceRoot);
30
+ const project = resolveProject({
31
+ config,
32
+ workspaceRoot,
33
+ projectName,
34
+ commandName,
35
+ usageLine: options.usageLine,
36
+ });
37
+
38
+ return runBuild(project, options);
39
+ }
40
+
41
+ /**
42
+ * 解決済みプロジェクトに対して clean → render → copy を実行します。
43
+ *
44
+ * @param {object} project
45
+ * @param {object} [options]
46
+ * @returns {Promise<object>}
47
+ */
48
+ async function runBuild(project, options = {}) {
49
+ const logTitle = options.logTitle || 'ビルドが完了しました';
50
+ const includeOutput = options.includeOutput !== false;
51
+ const shouldLog = options.log !== false;
52
+
53
+ if (project.build.clean) {
54
+ await cleanOutput({
55
+ outputDir: project.outputDir,
56
+ sourceDir: project.sourceDir,
57
+ workspaceRoot: project.workspaceRoot,
58
+ projectName: project.name,
59
+ });
60
+ }
61
+
62
+ const env = createNunjucksEnv({
63
+ sourceDir: project.sourceDir,
64
+ templates: project.templates,
65
+ });
66
+
67
+ const { renderedCount } = await renderPages({ env, project });
68
+ const { copiedCount } = await copyFiles({ project });
69
+
70
+ const outputDisplay = path
71
+ .relative(project.workspaceRoot, project.outputDir)
72
+ .split(path.sep)
73
+ .join('/');
74
+
75
+ const result = {
76
+ project,
77
+ renderedCount,
78
+ copiedCount,
79
+ outputDisplay: outputDisplay || project.outputDir,
80
+ };
81
+
82
+ if (shouldLog) {
83
+ printBuildResult(result, { logTitle, includeOutput });
84
+ }
85
+
86
+ return result;
87
+ }
88
+
89
+ function printBuildResult(result, options = {}) {
90
+ const logTitle = options.logTitle || 'ビルドが完了しました';
91
+ const includeOutput = options.includeOutput !== false;
92
+
93
+ console.log(`[JSKim] ${logTitle}`);
94
+ console.log(`プロジェクト: ${result.project.name}`);
95
+ console.log(`レンダリングしたページ数: ${result.renderedCount}`);
96
+ console.log(`コピーしたファイル数: ${result.copiedCount}`);
97
+ if (includeOutput) {
98
+ console.log(`出力先: ${result.outputDisplay}`);
99
+ }
100
+ }
101
+
102
+ module.exports = {
103
+ buildProject,
104
+ runBuild,
105
+ printBuildResult,
106
+ };