@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,130 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * defaults とプロジェクト設定をマージします。
5
+ *
6
+ * - スカラー: プロジェクト側があれば優先
7
+ * - オブジェクト (build / watch / serve / dev): 1段階の shallow merge
8
+ * - 配列 (render / templates / copy): プロジェクト側配列が defaults を丸ごと置き換え
9
+ *
10
+ * 元の defaults / プロジェクトオブジェクトは変更しません。
11
+ *
12
+ * @param {object} defaults
13
+ * @param {object} project
14
+ * @returns {object}
15
+ */
16
+ function mergeConfig(defaults, project) {
17
+ const base = defaults && typeof defaults === 'object' ? defaults : {};
18
+ const override = project && typeof project === 'object' ? project : {};
19
+
20
+ const merged = {
21
+ sourceDir: pickScalar(override.sourceDir, base.sourceDir),
22
+ outputDir: pickScalar(override.outputDir, base.outputDir),
23
+ render: pickArray(override.render, base.render, []),
24
+ templates: pickArray(override.templates, base.templates, []),
25
+ copy: pickArray(override.copy, base.copy, []),
26
+ build: mergeBuild(base.build, override.build),
27
+ watch: mergeWatch(base.watch, override.watch),
28
+ serve: mergeServe(base.serve, override.serve),
29
+ dev: mergeDev(base.dev, override.dev),
30
+ };
31
+
32
+ return {
33
+ sourceDir: merged.sourceDir,
34
+ outputDir: merged.outputDir,
35
+ render: merged.render.map((rule) => ({ ...rule })),
36
+ templates: [...merged.templates],
37
+ copy: merged.copy.map((rule) => ({ ...rule })),
38
+ build: { ...merged.build },
39
+ watch: { ...merged.watch },
40
+ serve: { ...merged.serve },
41
+ dev: { ...merged.dev },
42
+ };
43
+ }
44
+
45
+ function pickScalar(projectValue, defaultValue) {
46
+ return projectValue !== undefined ? projectValue : defaultValue;
47
+ }
48
+
49
+ function pickArray(projectValue, defaultValue, fallback) {
50
+ if (Array.isArray(projectValue)) {
51
+ return projectValue.slice();
52
+ }
53
+ if (Array.isArray(defaultValue)) {
54
+ return defaultValue.slice();
55
+ }
56
+ return fallback.slice();
57
+ }
58
+
59
+ function mergeBuild(defaultBuild, projectBuild) {
60
+ const base =
61
+ defaultBuild && typeof defaultBuild === 'object' ? defaultBuild : {};
62
+ const override =
63
+ projectBuild && typeof projectBuild === 'object' ? projectBuild : {};
64
+
65
+ return {
66
+ clean:
67
+ override.clean !== undefined
68
+ ? Boolean(override.clean)
69
+ : base.clean !== undefined
70
+ ? Boolean(base.clean)
71
+ : true,
72
+ };
73
+ }
74
+
75
+ function mergeWatch(defaultWatch, projectWatch) {
76
+ const base =
77
+ defaultWatch && typeof defaultWatch === 'object' ? defaultWatch : {};
78
+ const override =
79
+ projectWatch && typeof projectWatch === 'object' ? projectWatch : {};
80
+
81
+ return {
82
+ debounce:
83
+ override.debounce !== undefined
84
+ ? override.debounce
85
+ : base.debounce !== undefined
86
+ ? base.debounce
87
+ : 150,
88
+ };
89
+ }
90
+
91
+ function mergeServe(defaultServe, projectServe) {
92
+ const base =
93
+ defaultServe && typeof defaultServe === 'object' ? defaultServe : {};
94
+ const override =
95
+ projectServe && typeof projectServe === 'object' ? projectServe : {};
96
+
97
+ return {
98
+ host:
99
+ override.host !== undefined
100
+ ? override.host
101
+ : base.host !== undefined
102
+ ? base.host
103
+ : '127.0.0.1',
104
+ port:
105
+ override.port !== undefined
106
+ ? override.port
107
+ : base.port !== undefined
108
+ ? base.port
109
+ : 3000,
110
+ };
111
+ }
112
+
113
+ function mergeDev(defaultDev, projectDev) {
114
+ const base = defaultDev && typeof defaultDev === 'object' ? defaultDev : {};
115
+ const override =
116
+ projectDev && typeof projectDev === 'object' ? projectDev : {};
117
+
118
+ return {
119
+ liveReload:
120
+ override.liveReload !== undefined
121
+ ? override.liveReload
122
+ : base.liveReload !== undefined
123
+ ? base.liveReload
124
+ : true,
125
+ };
126
+ }
127
+
128
+ module.exports = {
129
+ mergeConfig,
130
+ };
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const fse = require('fs-extra');
5
+ const fg = require('fast-glob');
6
+
7
+ /**
8
+ * 出力ファイルのディレクトリから outputDir までの相対パスとして rootPath を計算します。
9
+ * HTML 用に常に `/` 区切りを使います。
10
+ *
11
+ * @param {string} outputFilePath HTML ファイルの絶対パス
12
+ * @param {string} outputDir 出力ルートの絶対パス
13
+ * @returns {string} "./" | "../" | "../../" | ...
14
+ */
15
+ function computeRootPath(outputFilePath, outputDir) {
16
+ const outFileDir = path.dirname(path.resolve(outputFilePath));
17
+ const outRoot = path.resolve(outputDir);
18
+ let relative = path.relative(outFileDir, outRoot);
19
+
20
+ if (!relative || relative === '') {
21
+ return './';
22
+ }
23
+
24
+ relative = relative.split(path.sep).join('/');
25
+
26
+ if (!relative.endsWith('/')) {
27
+ relative += '/';
28
+ }
29
+
30
+ return relative;
31
+ }
32
+
33
+ /**
34
+ * render ルールに従ってページをレンダリングします。
35
+ *
36
+ * @param {object} options
37
+ * @param {import('nunjucks').Environment} options.env
38
+ * @param {object} options.project 解決済みプロジェクト
39
+ * @returns {Promise<{ renderedCount: number, files: string[] }>}
40
+ */
41
+ async function renderPages({ env, project }) {
42
+ const { name, sourceDir, outputDir, render } = project;
43
+ let renderedCount = 0;
44
+ const files = [];
45
+
46
+ for (let i = 0; i < render.length; i += 1) {
47
+ const rule = render[i];
48
+ const fromDir = path.resolve(sourceDir, rule.from);
49
+ const toDir = path.resolve(outputDir, rule.to || '');
50
+ const extension = rule.extension.startsWith('.')
51
+ ? rule.extension
52
+ : `.${rule.extension}`;
53
+
54
+ if (!(await fse.pathExists(fromDir))) {
55
+ throw new Error(
56
+ `[JSKim] プロジェクト "${name}" の render[${i}].from が存在しません。\n` +
57
+ `パス: ${fromDir}\n` +
58
+ `設定: render[${i}].from = ${rule.from}`
59
+ );
60
+ }
61
+
62
+ const matches = await fg(rule.include, {
63
+ cwd: fromDir,
64
+ onlyFiles: true,
65
+ dot: false,
66
+ absolute: false,
67
+ });
68
+
69
+ matches.sort();
70
+
71
+ for (const relativeMatch of matches) {
72
+ const sourceFile = path.join(fromDir, relativeMatch);
73
+ const parsed = path.parse(relativeMatch);
74
+ const outRelative = path.join(parsed.dir, `${parsed.name}${extension}`);
75
+ const outputFile = path.join(toDir, outRelative);
76
+
77
+ // Nunjucks loader 用に sourceDir からの相対パスへ
78
+ const templatePath = path
79
+ .relative(sourceDir, sourceFile)
80
+ .split(path.sep)
81
+ .join('/');
82
+
83
+ const rootPath = computeRootPath(outputFile, outputDir);
84
+
85
+ let html;
86
+ try {
87
+ html = env.render(templatePath, { rootPath });
88
+ } catch (err) {
89
+ const message = err && err.message ? err.message : String(err);
90
+ throw new Error(
91
+ `[JSKim] プロジェクト "${name}" の Nunjucks レンダリングに失敗しました。\n` +
92
+ `ソース: ${sourceFile}\n` +
93
+ `テンプレート: ${templatePath}\n` +
94
+ `原因: ${message}`
95
+ );
96
+ }
97
+
98
+ try {
99
+ await fse.ensureDir(path.dirname(outputFile));
100
+ await fse.writeFile(outputFile, html, 'utf8');
101
+ } catch (err) {
102
+ const message = err && err.message ? err.message : String(err);
103
+ throw new Error(
104
+ `[JSKim] プロジェクト "${name}" のレンダリング結果の書き込みに失敗しました。\n` +
105
+ `出力: ${outputFile}\n` +
106
+ `原因: ${message}`
107
+ );
108
+ }
109
+
110
+ renderedCount += 1;
111
+ files.push(outputFile);
112
+ }
113
+ }
114
+
115
+ return { renderedCount, files };
116
+ }
117
+
118
+ module.exports = {
119
+ renderPages,
120
+ computeRootPath,
121
+ };
@@ -0,0 +1,198 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { mergeConfig } = require('./merge-config');
6
+
7
+ /**
8
+ * 読み込んだ設定から名前付きプロジェクトを解決・検証します。
9
+ *
10
+ * @param {object} options
11
+ * @param {object} options.config
12
+ * @param {string} options.workspaceRoot
13
+ * @param {string|undefined} options.projectName
14
+ * @param {string} [options.commandName='build']
15
+ * @param {string} [options.usageLine] プロジェクト名欠落時の使用方法行
16
+ * @returns {object} 解決済みプロジェクト
17
+ */
18
+ function resolveProject({
19
+ config,
20
+ workspaceRoot,
21
+ projectName,
22
+ commandName = 'build',
23
+ usageLine,
24
+ }) {
25
+ if (!projectName || String(projectName).trim() === '') {
26
+ const usage =
27
+ usageLine || `npm run ${commandName} -- <project-name>`;
28
+ throw new Error(
29
+ `[JSKim] プロジェクト名を指定してください。\n` +
30
+ `使用方法: ${usage}`
31
+ );
32
+ }
33
+
34
+ const name = String(projectName).trim();
35
+ const projects = config.projects || {};
36
+ const projectConfig = projects[name];
37
+
38
+ if (!projectConfig) {
39
+ const available = Object.keys(projects);
40
+ const list =
41
+ available.length > 0 ? available.join(', ') : '(登録なし)';
42
+ throw new Error(
43
+ `[JSKim] 不明なプロジェクトです: ${name}\n` +
44
+ `利用可能なプロジェクト: ${list}`
45
+ );
46
+ }
47
+
48
+ const merged = mergeConfig(config.defaults || {}, projectConfig);
49
+
50
+ if (!merged.sourceDir || String(merged.sourceDir).trim() === '') {
51
+ throw new Error(
52
+ `[JSKim] プロジェクト "${name}" に sourceDir がありません。\n` +
53
+ `jskim.config.js の projects.${name}.sourceDir を設定してください。`
54
+ );
55
+ }
56
+
57
+ if (!merged.outputDir || String(merged.outputDir).trim() === '') {
58
+ throw new Error(
59
+ `[JSKim] プロジェクト "${name}" に outputDir がありません。\n` +
60
+ `jskim.config.js の projects.${name}.outputDir を設定してください。`
61
+ );
62
+ }
63
+
64
+ const sourceDir = path.resolve(workspaceRoot, merged.sourceDir);
65
+ const outputDir = path.resolve(workspaceRoot, merged.outputDir);
66
+
67
+ if (!fs.existsSync(sourceDir)) {
68
+ throw new Error(
69
+ `[JSKim] プロジェクト "${name}" の sourceDir が存在しません。\n` +
70
+ `パス: ${sourceDir}\n` +
71
+ `設定: projects.${name}.sourceDir = ${merged.sourceDir}`
72
+ );
73
+ }
74
+
75
+ if (!fs.statSync(sourceDir).isDirectory()) {
76
+ throw new Error(
77
+ `[JSKim] プロジェクト "${name}" の sourceDir はディレクトリではありません。\n` +
78
+ `パス: ${sourceDir}`
79
+ );
80
+ }
81
+
82
+ if (!Array.isArray(merged.render) || merged.render.length === 0) {
83
+ throw new Error(
84
+ `[JSKim] プロジェクト "${name}" の render 設定が無い、または空です。\n` +
85
+ `jskim.config.js の defaults.render または projects.${name}.render を定義してください。`
86
+ );
87
+ }
88
+
89
+ for (let i = 0; i < merged.render.length; i += 1) {
90
+ const rule = merged.render[i];
91
+ if (!rule || typeof rule !== 'object') {
92
+ throw new Error(
93
+ `[JSKim] プロジェクト "${name}" の render[${i}] が不正です。\n` +
94
+ `原因: 各 render ルールはオブジェクトである必要があります。`
95
+ );
96
+ }
97
+ if (!rule.from || String(rule.from).trim() === '') {
98
+ throw new Error(
99
+ `[JSKim] プロジェクト "${name}" の render[${i}].from が不正です。\n` +
100
+ `原因: from は必須です(sourceDir 基準)。`
101
+ );
102
+ }
103
+ if (!Array.isArray(rule.include) || rule.include.length === 0) {
104
+ throw new Error(
105
+ `[JSKim] プロジェクト "${name}" の render[${i}].include が不正です。\n` +
106
+ `原因: include は空でない glob パターン配列である必要があります。`
107
+ );
108
+ }
109
+ if (!rule.extension || String(rule.extension).trim() === '') {
110
+ throw new Error(
111
+ `[JSKim] プロジェクト "${name}" の render[${i}].extension が不正です。\n` +
112
+ `原因: extension は必須です(例: ".html")。`
113
+ );
114
+ }
115
+ }
116
+
117
+ validateWatchConfig(merged.watch, name);
118
+ validateServeConfig(merged.serve, name);
119
+ validateDevConfig(merged.dev, name);
120
+
121
+ return {
122
+ name,
123
+ sourceDir,
124
+ outputDir,
125
+ sourceDirConfig: merged.sourceDir,
126
+ outputDirConfig: merged.outputDir,
127
+ render: merged.render,
128
+ templates: merged.templates,
129
+ copy: merged.copy,
130
+ build: merged.build,
131
+ watch: merged.watch,
132
+ serve: merged.serve,
133
+ dev: merged.dev,
134
+ workspaceRoot,
135
+ };
136
+ }
137
+
138
+ function validateWatchConfig(watch, projectName) {
139
+ const debounce = watch && watch.debounce;
140
+
141
+ if (
142
+ typeof debounce !== 'number' ||
143
+ !Number.isFinite(debounce) ||
144
+ debounce < 0
145
+ ) {
146
+ throw new Error(
147
+ `[JSKim] 設定値が不正です: watch.debounce\n` +
148
+ `プロジェクト: ${projectName}\n` +
149
+ `0以上の有限な数値を指定してください。\n` +
150
+ `受け取った値: ${String(debounce)}`
151
+ );
152
+ }
153
+ }
154
+
155
+ function validateServeConfig(serve, projectName) {
156
+ const host = serve && serve.host;
157
+ const port = serve && serve.port;
158
+
159
+ if (typeof host !== 'string' || host.trim() === '') {
160
+ throw new Error(
161
+ `[JSKim] 設定値が不正です: serve.host\n` +
162
+ `プロジェクト: ${projectName}\n` +
163
+ `空でない文字列を指定してください。\n` +
164
+ `受け取った値: ${String(host)}`
165
+ );
166
+ }
167
+
168
+ if (
169
+ typeof port !== 'number' ||
170
+ !Number.isInteger(port) ||
171
+ port < 1 ||
172
+ port > 65535
173
+ ) {
174
+ throw new Error(
175
+ `[JSKim] 設定値が不正です: serve.port\n` +
176
+ `プロジェクト: ${projectName}\n` +
177
+ `1から65535までの整数を指定してください。\n` +
178
+ `受け取った値: ${String(port)}`
179
+ );
180
+ }
181
+ }
182
+
183
+ function validateDevConfig(dev, projectName) {
184
+ const liveReload = dev && dev.liveReload;
185
+
186
+ if (typeof liveReload !== 'boolean') {
187
+ throw new Error(
188
+ `[JSKim] 設定値が不正です: dev.liveReload\n` +
189
+ `プロジェクト: ${projectName}\n` +
190
+ `boolean を指定してください。\n` +
191
+ `受け取った値: ${String(liveReload)}`
192
+ );
193
+ }
194
+ }
195
+
196
+ module.exports = {
197
+ resolveProject,
198
+ };
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ /**
7
+ * 解決済みプロジェクト設定から監視ディレクトリを計算します。
8
+ * sourceDir 配下の render[].from / templates[] / copy[].from を監視します。
9
+ * outputDir / dist / node_modules は監視しません。
10
+ *
11
+ * @param {object} project
12
+ * @returns {{ absolutePaths: string[], displayPaths: string[] }}
13
+ */
14
+ function resolveWatchPaths(project) {
15
+ const { name, sourceDir, outputDir, workspaceRoot, render, templates, copy } =
16
+ project;
17
+
18
+ const absolutePaths = [];
19
+ const seen = new Set();
20
+
21
+ function addCandidate(absPath) {
22
+ const resolved = path.resolve(absPath);
23
+ const key =
24
+ process.platform === 'win32' ? resolved.toLowerCase() : resolved;
25
+
26
+ if (seen.has(key)) {
27
+ return;
28
+ }
29
+
30
+ if (isForbiddenWatchPath(resolved, outputDir, workspaceRoot)) {
31
+ return;
32
+ }
33
+
34
+ if (!fs.existsSync(resolved)) {
35
+ return;
36
+ }
37
+
38
+ seen.add(key);
39
+ absolutePaths.push(resolved);
40
+ }
41
+
42
+ for (const rule of render) {
43
+ if (rule && rule.from) {
44
+ addCandidate(path.join(sourceDir, rule.from));
45
+ }
46
+ }
47
+
48
+ for (const rel of Array.isArray(templates) ? templates : []) {
49
+ if (rel) {
50
+ addCandidate(path.join(sourceDir, rel));
51
+ }
52
+ }
53
+
54
+ for (const rule of Array.isArray(copy) ? copy : []) {
55
+ if (rule && rule.from) {
56
+ addCandidate(path.join(sourceDir, rule.from));
57
+ }
58
+ }
59
+
60
+ if (absolutePaths.length === 0) {
61
+ throw new Error(
62
+ `[JSKim] プロジェクト "${name}" の監視パスがありません。\n` +
63
+ `原因: render[].from / templates[] / copy[].from のいずれも既存パスに解決できませんでした。\n` +
64
+ `sourceDir: ${sourceDir}`
65
+ );
66
+ }
67
+
68
+ absolutePaths.sort(comparePaths);
69
+
70
+ const displayPaths = absolutePaths.map((abs) =>
71
+ toDisplayPath(abs, workspaceRoot)
72
+ );
73
+
74
+ return { absolutePaths, displayPaths };
75
+ }
76
+
77
+ function isForbiddenWatchPath(candidate, outputDir, workspaceRoot) {
78
+ const resolved = path.resolve(candidate);
79
+ const out = path.resolve(outputDir);
80
+ const root = path.resolve(workspaceRoot);
81
+ const distRoot = path.join(root, 'dist');
82
+
83
+ if (samePath(resolved, out) || isInsideOrSame(out, resolved)) {
84
+ return true;
85
+ }
86
+
87
+ if (samePath(resolved, distRoot) || isInsideOrSame(distRoot, resolved)) {
88
+ return true;
89
+ }
90
+
91
+ const parts = resolved.split(path.sep);
92
+ if (parts.some((part) => part.toLowerCase() === 'node_modules')) {
93
+ return true;
94
+ }
95
+
96
+ return false;
97
+ }
98
+
99
+ function toDisplayPath(abs, workspaceRoot) {
100
+ const rel = path.relative(workspaceRoot, abs);
101
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
102
+ return abs.split(path.sep).join('/');
103
+ }
104
+ return rel.split(path.sep).join('/');
105
+ }
106
+
107
+ function samePath(a, b) {
108
+ const na = path.resolve(a);
109
+ const nb = path.resolve(b);
110
+ if (process.platform === 'win32') {
111
+ return na.toLowerCase() === nb.toLowerCase();
112
+ }
113
+ return na === nb;
114
+ }
115
+
116
+ function isInsideOrSame(parent, child) {
117
+ if (samePath(parent, child)) {
118
+ return true;
119
+ }
120
+ const rel = path.relative(path.resolve(parent), path.resolve(child));
121
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
122
+ }
123
+
124
+ function comparePaths(a, b) {
125
+ const left = process.platform === 'win32' ? a.toLowerCase() : a;
126
+ const right = process.platform === 'win32' ? b.toLowerCase() : b;
127
+ if (left < right) return -1;
128
+ if (left > right) return 1;
129
+ return 0;
130
+ }
131
+
132
+ module.exports = {
133
+ resolveWatchPaths,
134
+ };
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ const { runServeCommand } = require('./commands/serve-command');
4
+
5
+ runServeCommand({
6
+ projectName: process.argv[2],
7
+ workspaceRoot: process.cwd(),
8
+ usageLine: 'npm run serve -- <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,13 @@
1
+ 'use strict';
2
+
3
+ const { runWatchCommand } = require('./commands/watch-command');
4
+
5
+ runWatchCommand({
6
+ projectName: process.argv[2],
7
+ workspaceRoot: process.cwd(),
8
+ usageLine: 'npm run watch -- <project-name>',
9
+ }).catch((err) => {
10
+ const message = err && err.message ? err.message : String(err);
11
+ console.error(message);
12
+ process.exitCode = 1;
13
+ });