@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.
- package/LICENSE +21 -0
- package/README.md +336 -0
- package/bin/jskim.js +105 -0
- package/docs/configuration.md +322 -0
- package/docs/create-jskim.md +80 -0
- package/docs/publishing.md +134 -0
- package/package.json +57 -0
- package/scripts/build.js +13 -0
- package/scripts/commands/build-command.js +29 -0
- package/scripts/commands/dev-command.js +164 -0
- package/scripts/commands/help-text.js +46 -0
- package/scripts/commands/path-display.js +21 -0
- package/scripts/commands/register-shutdown.js +32 -0
- package/scripts/commands/serve-command.js +88 -0
- package/scripts/commands/serve-errors.js +82 -0
- package/scripts/commands/watch-command.js +71 -0
- package/scripts/dev.js +13 -0
- package/scripts/lib/build-project.js +106 -0
- package/scripts/lib/clean-output.js +119 -0
- package/scripts/lib/copy-files.js +116 -0
- package/scripts/lib/create-live-reload.js +201 -0
- package/scripts/lib/create-nunjucks-env.js +61 -0
- package/scripts/lib/create-project-watcher.js +272 -0
- package/scripts/lib/create-static-server.js +351 -0
- package/scripts/lib/load-config.js +64 -0
- package/scripts/lib/merge-config.js +130 -0
- package/scripts/lib/render-pages.js +121 -0
- package/scripts/lib/resolve-project.js +198 -0
- package/scripts/lib/resolve-watch-paths.js +134 -0
- package/scripts/serve.js +13 -0
- package/scripts/watch.js +13 -0
|
@@ -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
|
+
};
|
package/scripts/serve.js
ADDED
|
@@ -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
|
+
});
|
package/scripts/watch.js
ADDED
|
@@ -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
|
+
});
|