nibula 1.2.2 → 1.2.4
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/.eleventy.js +0 -5
- package/CHANGELOG.md +21 -9
- package/README.md +1 -1
- package/bin/create.js +1 -2
- package/bin/nibula.js +2 -5
- package/nginx.conf +75 -75
- package/package.json +74 -74
- package/src/backend/_core/index.js +267 -267
- package/src/backend/_core/init.js +52 -52
- package/src/backend/_core/modules/RateLimiter.js +58 -58
- package/src/backend/_core/modules/Response.js +59 -59
- package/src/backend/api/protected/example-protected.js +23 -23
- package/src/backend/api/public/example-public.js +24 -24
- package/src/backend/backend-node.service.example +30 -30
- package/src/backend/database/Database.js +46 -46
- package/src/backend/example.config.js +37 -37
- package/src/backend/package.json +18 -18
- package/src/frontend/.htaccess +51 -51
- package/src/frontend/assets/brand/favicon.svg +54 -54
- package/src/frontend/assets/brand/logo.svg +54 -54
- package/src/frontend/data/site.json +48 -48
- package/src/frontend/web.config +55 -55
- package/tools/assistant.js +122 -151
- package/tools/buildJs.js +39 -37
- package/tools/cleanOutput.js +23 -25
- package/tools/cli/prompt.js +40 -0
- package/tools/cli/ui.js +74 -0
- package/tools/config/messages.json +76 -0
- package/tools/config/settings.json +135 -0
- package/tools/lib/colors.js +17 -0
- package/tools/lib/files.js +65 -0
- package/tools/lib/logger.js +22 -0
- package/tools/lib/outputPath.js +141 -0
- package/tools/lib/pageActions.js +138 -0
- package/tools/lib/pageArtifacts.js +46 -0
- package/tools/lib/pageComponents.js +88 -0
- package/tools/lib/paths.js +61 -0
- package/tools/lib/project.js +54 -0
- package/tools/lib/siteData.js +93 -0
- package/tools/lib/text.js +47 -0
- package/tools/lib/validation.js +39 -0
- package/tools/res/templates/template.js +3 -1
- package/tools/res/templates/template.ts +3 -1
- package/tools/modules/constants.js +0 -66
- package/tools/modules/pageComponents.js +0 -77
- package/tools/modules/updateData.js +0 -90
- package/tools/modules/updateOutputPath.js +0 -112
- package/tools/modules/updatePage.js +0 -162
- package/tools/modules/utils.js +0 -27
- package/tools/modules/validation.js +0 -30
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const settings = require('../config/settings.json');
|
|
4
|
+
const { paint } = require('./colors');
|
|
5
|
+
const { message } = require('./logger');
|
|
6
|
+
|
|
7
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
|
|
8
|
+
const PATH_SEPARATOR = '/';
|
|
9
|
+
const NOT_INSIDE_PROJECT_MESSAGE = paint('red', message('project.notInside'));
|
|
10
|
+
|
|
11
|
+
let cachedProjectRoot = null;
|
|
12
|
+
|
|
13
|
+
function findProjectRoot(start) {
|
|
14
|
+
let directory = path.resolve(start ?? process.cwd());
|
|
15
|
+
while (true) {
|
|
16
|
+
if (fs.existsSync(path.join(directory, settings.project.marker))) return directory;
|
|
17
|
+
const parent = path.dirname(directory);
|
|
18
|
+
if (parent === directory) return null;
|
|
19
|
+
directory = parent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function projectRoot() {
|
|
24
|
+
if (cachedProjectRoot) return cachedProjectRoot;
|
|
25
|
+
const root = findProjectRoot();
|
|
26
|
+
if (!root) {
|
|
27
|
+
console.error(NOT_INSIDE_PROJECT_MESSAGE);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
cachedProjectRoot = root;
|
|
31
|
+
return root;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveFromProject(relativePath) {
|
|
35
|
+
return path.join(projectRoot(), ...relativePath.split(PATH_SEPARATOR));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveFromPackage(relativePath) {
|
|
39
|
+
return path.join(PACKAGE_ROOT, ...relativePath.split(PATH_SEPARATOR));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PATHS = Object.freeze({
|
|
43
|
+
get root() { return projectRoot(); },
|
|
44
|
+
get eleventyConfig() { return resolveFromProject(settings.paths.eleventyConfig); },
|
|
45
|
+
get packageJson() { return resolveFromProject(settings.paths.packageJson); },
|
|
46
|
+
get typescriptConfig() { return resolveFromProject(settings.paths.typescriptConfig); },
|
|
47
|
+
get routes() { return resolveFromProject(settings.paths.routes); },
|
|
48
|
+
get stylePages() { return resolveFromProject(settings.paths.stylePages); },
|
|
49
|
+
get siteData() { return resolveFromProject(settings.paths.siteData); },
|
|
50
|
+
get pageComponents() { return resolveFromProject(settings.paths.pageComponents); },
|
|
51
|
+
get templates() { return resolveFromPackage(settings.paths.templates); },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
PATHS,
|
|
56
|
+
PACKAGE_ROOT,
|
|
57
|
+
NOT_INSIDE_PROJECT_MESSAGE,
|
|
58
|
+
findProjectRoot,
|
|
59
|
+
resolveFromProject,
|
|
60
|
+
resolveFromPackage,
|
|
61
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const settings = require('../config/settings.json');
|
|
3
|
+
const { PATHS, resolveFromProject } = require('./paths');
|
|
4
|
+
const { readJson, writeJson } = require('./files');
|
|
5
|
+
|
|
6
|
+
const JAVASCRIPT = 'javascript';
|
|
7
|
+
const TYPESCRIPT = 'typescript';
|
|
8
|
+
const ENTRY_WILDCARD = '*';
|
|
9
|
+
|
|
10
|
+
function isTypeScriptProject() {
|
|
11
|
+
return fs.existsSync(PATHS.typescriptConfig);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function currentLanguage() {
|
|
15
|
+
return isTypeScriptProject() ? TYPESCRIPT : JAVASCRIPT;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function languageSettings(language) {
|
|
19
|
+
return settings.languages[language ?? currentLanguage()];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scriptPagesDirectory(language) {
|
|
23
|
+
return resolveFromProject(languageSettings(language).pages);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function scriptEntries(language) {
|
|
27
|
+
const { pages, extension } = languageSettings(language);
|
|
28
|
+
return `${pages}/${ENTRY_WILDCARD}.${extension}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function allScriptEntries() {
|
|
32
|
+
return Object.keys(settings.languages).map((language) => scriptEntries(language));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readPackageJson() {
|
|
36
|
+
return readJson(PATHS.packageJson);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writePackageJson(content) {
|
|
40
|
+
writeJson(PATHS.packageJson, content);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
JAVASCRIPT,
|
|
45
|
+
TYPESCRIPT,
|
|
46
|
+
isTypeScriptProject,
|
|
47
|
+
currentLanguage,
|
|
48
|
+
languageSettings,
|
|
49
|
+
scriptPagesDirectory,
|
|
50
|
+
scriptEntries,
|
|
51
|
+
allScriptEntries,
|
|
52
|
+
readPackageJson,
|
|
53
|
+
writePackageJson,
|
|
54
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const settings = require('../config/settings.json');
|
|
3
|
+
const { PATHS } = require('./paths');
|
|
4
|
+
const { exists, readText, writeJson } = require('./files');
|
|
5
|
+
const { log } = require('./logger');
|
|
6
|
+
const { toCamelCase, toTitleCase, formatValue } = require('./text');
|
|
7
|
+
|
|
8
|
+
const PAGES_KEY = settings.page.dataKeys.pages;
|
|
9
|
+
const SEO_KEY = settings.page.dataKeys.seo;
|
|
10
|
+
const TITLE_KEY = settings.page.dataKeys.title;
|
|
11
|
+
|
|
12
|
+
function readSiteData() {
|
|
13
|
+
if (!exists(PATHS.siteData)) {
|
|
14
|
+
log('siteData.fileMissing', { path: PATHS.siteData });
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(readText(PATHS.siteData));
|
|
19
|
+
if (!data[PAGES_KEY] || typeof data[PAGES_KEY] !== 'object') data[PAGES_KEY] = {};
|
|
20
|
+
return data;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
log('siteData.parseFailed', { file: path.basename(PATHS.siteData), error: error.message });
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeSiteData(data) {
|
|
28
|
+
writeJson(PATHS.siteData, data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createRecord(pageName) {
|
|
32
|
+
return formatValue(settings.page.defaultData, { title: toTitleCase(pageName) });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function addSiteData(pageName) {
|
|
36
|
+
const data = readSiteData();
|
|
37
|
+
if (!data) return;
|
|
38
|
+
|
|
39
|
+
const camelName = toCamelCase(pageName);
|
|
40
|
+
if (data[PAGES_KEY][camelName]) {
|
|
41
|
+
log('siteData.recordExists', { name: camelName });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
data[PAGES_KEY][camelName] = createRecord(pageName);
|
|
46
|
+
|
|
47
|
+
writeSiteData(data);
|
|
48
|
+
log('siteData.recordAdded', { name: camelName });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function removeSiteData(pageName) {
|
|
52
|
+
const data = readSiteData();
|
|
53
|
+
if (!data) return;
|
|
54
|
+
|
|
55
|
+
const camelName = toCamelCase(pageName);
|
|
56
|
+
if (!data[PAGES_KEY][camelName]) {
|
|
57
|
+
log('siteData.recordMissing', { name: camelName });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
delete data[PAGES_KEY][camelName];
|
|
62
|
+
writeSiteData(data);
|
|
63
|
+
log('siteData.recordRemoved', { name: camelName });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renameSiteData(oldName, newName) {
|
|
67
|
+
const data = readSiteData();
|
|
68
|
+
if (!data) return;
|
|
69
|
+
|
|
70
|
+
const oldCamelName = toCamelCase(oldName);
|
|
71
|
+
const newCamelName = toCamelCase(newName);
|
|
72
|
+
|
|
73
|
+
if (!data[PAGES_KEY][oldCamelName]) {
|
|
74
|
+
log('siteData.recordMissing', { name: oldCamelName });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (data[PAGES_KEY][newCamelName]) {
|
|
78
|
+
log('siteData.recordExists', { name: newCamelName });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const record = data[PAGES_KEY][oldCamelName];
|
|
83
|
+
data[PAGES_KEY][newCamelName] = {
|
|
84
|
+
...record,
|
|
85
|
+
[SEO_KEY]: { ...record[SEO_KEY], [TITLE_KEY]: toTitleCase(newName) },
|
|
86
|
+
};
|
|
87
|
+
delete data[PAGES_KEY][oldCamelName];
|
|
88
|
+
|
|
89
|
+
writeSiteData(data);
|
|
90
|
+
log('siteData.recordRenamed', { source: oldCamelName, destination: newCamelName });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { addSiteData, removeSiteData, renameSiteData };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const PLACEHOLDER_PATTERN = /\{(\w+)\}/g;
|
|
2
|
+
const CAMEL_SEPARATOR_PATTERN = /[-_][a-z0-9]/g;
|
|
3
|
+
const INVALID_KEBAB_PATTERN = /[^a-z0-9\s_-]/g;
|
|
4
|
+
const KEBAB_SEPARATOR_PATTERN = /[\s_]+/g;
|
|
5
|
+
const REPEATED_HYPHEN_PATTERN = /-+/g;
|
|
6
|
+
const EDGE_HYPHEN_PATTERN = /^-+|-+$/g;
|
|
7
|
+
const WORD_SEPARATOR = '-';
|
|
8
|
+
const SPACE = ' ';
|
|
9
|
+
const EMPTY = '';
|
|
10
|
+
|
|
11
|
+
function formatText(template, values = {}) {
|
|
12
|
+
return String(template).replace(PLACEHOLDER_PATTERN, (match, key) =>
|
|
13
|
+
Object.prototype.hasOwnProperty.call(values, key) ? String(values[key]) : match
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatValue(value, values = {}) {
|
|
18
|
+
if (typeof value === 'string') return formatText(value, values);
|
|
19
|
+
if (Array.isArray(value)) return value.map((item) => formatValue(item, values));
|
|
20
|
+
if (value !== null && typeof value === 'object') {
|
|
21
|
+
return Object.fromEntries(
|
|
22
|
+
Object.entries(value).map(([key, item]) => [key, formatValue(item, values)])
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toCamelCase(value) {
|
|
29
|
+
return value.toLowerCase().replace(CAMEL_SEPARATOR_PATTERN, (group) => group.slice(1).toUpperCase());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toKebabCase(value) {
|
|
33
|
+
return value.trim().toLowerCase()
|
|
34
|
+
.replace(INVALID_KEBAB_PATTERN, EMPTY)
|
|
35
|
+
.replace(KEBAB_SEPARATOR_PATTERN, WORD_SEPARATOR)
|
|
36
|
+
.replace(REPEATED_HYPHEN_PATTERN, WORD_SEPARATOR)
|
|
37
|
+
.replace(EDGE_HYPHEN_PATTERN, EMPTY);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toTitleCase(value) {
|
|
41
|
+
return value.split(WORD_SEPARATOR)
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
44
|
+
.join(SPACE);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { formatText, formatValue, toCamelCase, toKebabCase, toTitleCase };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const settings = require('../config/settings.json');
|
|
2
|
+
const { PATHS } = require('./paths');
|
|
3
|
+
const { exists } = require('./files');
|
|
4
|
+
const { message } = require('./logger');
|
|
5
|
+
|
|
6
|
+
const NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
7
|
+
const LEADING_DIGIT_PATTERN = /^\d/;
|
|
8
|
+
const INVALID_PATH_PATTERN = /[<>|?*"']/;
|
|
9
|
+
|
|
10
|
+
function requiredFiles() {
|
|
11
|
+
return [
|
|
12
|
+
{ label: settings.paths.eleventyConfig, path: PATHS.eleventyConfig },
|
|
13
|
+
{ label: settings.paths.packageJson, path: PATHS.packageJson },
|
|
14
|
+
{ label: settings.paths.siteData, path: PATHS.siteData },
|
|
15
|
+
{ label: settings.paths.pageComponents, path: PATHS.pageComponents },
|
|
16
|
+
{ label: message('validation.templatesLabel'), path: PATHS.templates },
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validatePageName(name) {
|
|
21
|
+
if (!name) return message('validation.invalidName');
|
|
22
|
+
if (!NAME_PATTERN.test(name)) return message('validation.invalidNameCharacters');
|
|
23
|
+
if (LEADING_DIGIT_PATTERN.test(name)) return message('validation.nameStartsWithNumber');
|
|
24
|
+
if (settings.page.protected.includes(name)) return message('validation.protectedName', { name });
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateOutputPath(input) {
|
|
29
|
+
const value = (input ?? '').trim();
|
|
30
|
+
if (!value) return message('validation.invalidPath');
|
|
31
|
+
if (INVALID_PATH_PATTERN.test(value)) return message('validation.invalidPathCharacters');
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function checkRequiredFiles() {
|
|
36
|
+
return requiredFiles().filter((item) => !exists(item.path));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { validatePageName, validateOutputPath, checkRequiredFiles };
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
|
|
5
|
-
const TEMPLATES_DIR = path.join(PACKAGE_ROOT, 'tools', 'res', 'templates');
|
|
6
|
-
|
|
7
|
-
const PROJECT_MARKER = '.eleventy.js';
|
|
8
|
-
|
|
9
|
-
const PROTECTED_PAGES = Object.freeze(['homepage', '404']);
|
|
10
|
-
|
|
11
|
-
const color = Object.freeze({
|
|
12
|
-
reset: '\x1b[0m',
|
|
13
|
-
bold: '\x1b[1m',
|
|
14
|
-
dim: '\x1b[2m',
|
|
15
|
-
red: '\x1b[31m',
|
|
16
|
-
green: '\x1b[32m',
|
|
17
|
-
yellow: '\x1b[33m',
|
|
18
|
-
blue: '\x1b[34m',
|
|
19
|
-
magenta: '\x1b[35m',
|
|
20
|
-
cyan: '\x1b[36m',
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const NOT_INSIDE_PROJECT_MESSAGE = `${color.red}Not inside a Nibula project.${color.reset}`;
|
|
24
|
-
|
|
25
|
-
function findProjectRoot(start) {
|
|
26
|
-
let dir = path.resolve(start ?? process.cwd());
|
|
27
|
-
while (true) {
|
|
28
|
-
if (fs.existsSync(path.join(dir, PROJECT_MARKER))) return dir;
|
|
29
|
-
const parent = path.dirname(dir);
|
|
30
|
-
if (parent === dir) return null;
|
|
31
|
-
dir = parent;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let cachedRoot = null;
|
|
36
|
-
|
|
37
|
-
function projectRoot() {
|
|
38
|
-
if (cachedRoot) return cachedRoot;
|
|
39
|
-
const root = findProjectRoot();
|
|
40
|
-
if (!root) {
|
|
41
|
-
console.error(NOT_INSIDE_PROJECT_MESSAGE);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
cachedRoot = root;
|
|
45
|
-
return root;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function frontendDir() {
|
|
49
|
-
return path.join(projectRoot(), 'src', 'frontend');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const PATHS = Object.freeze({
|
|
53
|
-
get root() { return projectRoot(); },
|
|
54
|
-
get routes() { return path.join(frontendDir(), 'routes'); },
|
|
55
|
-
get scssPages() { return path.join(frontendDir(), 'scss', 'pages'); },
|
|
56
|
-
get jsPages() { return path.join(frontendDir(), 'js', 'pages'); },
|
|
57
|
-
get tsPages() { return path.join(frontendDir(), 'ts', 'pages'); },
|
|
58
|
-
get siteData() { return path.join(frontendDir(), 'data', 'site.json'); },
|
|
59
|
-
get pageComponents() { return path.join(frontendDir(), 'layouts', 'page-components.njk'); },
|
|
60
|
-
get eleventyConfig() { return path.join(projectRoot(), '.eleventy.js'); },
|
|
61
|
-
get packageJson() { return path.join(projectRoot(), 'package.json'); },
|
|
62
|
-
get tsconfig() { return path.join(projectRoot(), 'tsconfig.json'); },
|
|
63
|
-
templates: TEMPLATES_DIR,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
module.exports = { PATHS, PROJECT_MARKER, PROTECTED_PAGES, color, NOT_INSIDE_PROJECT_MESSAGE, findProjectRoot };
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const { PATHS } = require('./constants');
|
|
3
|
-
const { toCamelCase } = require('./utils');
|
|
4
|
-
|
|
5
|
-
function readPageComponents() {
|
|
6
|
-
if (!fs.existsSync(PATHS.pageComponents)) {
|
|
7
|
-
console.log(`[skip] not found: ${PATHS.pageComponents}`);
|
|
8
|
-
return null;
|
|
9
|
-
}
|
|
10
|
-
return fs.readFileSync(PATHS.pageComponents, 'utf8');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function writePageComponents(content) {
|
|
14
|
-
fs.writeFileSync(PATHS.pageComponents, content);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function elifBlockRegex(camelName) {
|
|
18
|
-
return new RegExp(
|
|
19
|
-
`[ \\t]*\\{%\\s*elif\\s+title\\s*==\\s*"${camelName}"\\s*%\\}[\\s\\S]*?(?=[ \\t]*\\{%\\s*(?:elif|else|endif))`,
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function addPageBlock(pageName) {
|
|
24
|
-
const content = readPageComponents();
|
|
25
|
-
if (!content) return;
|
|
26
|
-
|
|
27
|
-
const camelName = toCamelCase(pageName);
|
|
28
|
-
if (content.includes(`{% elif title == "${camelName}" %}`)) return;
|
|
29
|
-
|
|
30
|
-
if (!content.includes('{% else %}')) {
|
|
31
|
-
console.log('[skip] no {% else %} anchor in page-components.njk');
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const block =
|
|
36
|
-
`{% elif title == "${camelName}" %}\n` +
|
|
37
|
-
`{% include "component.njk" ignore missing %}\n\n`;
|
|
38
|
-
|
|
39
|
-
writePageComponents(content.replace('{% else %}', `${block}{% else %}`));
|
|
40
|
-
console.log(`[updated] page block added for "${camelName}"`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function removePageBlock(pageName) {
|
|
44
|
-
const content = readPageComponents();
|
|
45
|
-
if (!content) return;
|
|
46
|
-
|
|
47
|
-
const camelName = toCamelCase(pageName);
|
|
48
|
-
if (!elifBlockRegex(camelName).test(content)) {
|
|
49
|
-
console.log(`[skip] page block for "${camelName}" not found`);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const updated = content
|
|
54
|
-
.replace(elifBlockRegex(camelName), '')
|
|
55
|
-
.replace(/\n\s*\n\s*\n/g, '\n\n');
|
|
56
|
-
|
|
57
|
-
writePageComponents(updated);
|
|
58
|
-
console.log(`[cleaned] page block removed for "${camelName}"`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function renamePageBlock(oldName, newName) {
|
|
62
|
-
const content = readPageComponents();
|
|
63
|
-
if (!content) return;
|
|
64
|
-
|
|
65
|
-
const oldLine = `{% elif title == "${toCamelCase(oldName)}" %}`;
|
|
66
|
-
const newLine = `{% elif title == "${toCamelCase(newName)}" %}`;
|
|
67
|
-
|
|
68
|
-
if (!content.includes(oldLine)) {
|
|
69
|
-
console.log(`[skip] page block for "${toCamelCase(oldName)}" not found`);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
writePageComponents(content.replace(oldLine, newLine));
|
|
74
|
-
console.log(`[renamed] page block "${toCamelCase(oldName)}" → "${toCamelCase(newName)}"`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
module.exports = { addPageBlock, removePageBlock, renamePageBlock };
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const { PATHS } = require('./constants');
|
|
3
|
-
const { toCamelCase, toNiceTitle } = require('./utils');
|
|
4
|
-
|
|
5
|
-
function readSiteData() {
|
|
6
|
-
if (!fs.existsSync(PATHS.siteData)) {
|
|
7
|
-
console.log(`[skip] not found: ${PATHS.siteData}`);
|
|
8
|
-
return null;
|
|
9
|
-
}
|
|
10
|
-
try {
|
|
11
|
-
const data = JSON.parse(fs.readFileSync(PATHS.siteData, 'utf8'));
|
|
12
|
-
if (!data.pages || typeof data.pages !== 'object') data.pages = {};
|
|
13
|
-
return data;
|
|
14
|
-
} catch (err) {
|
|
15
|
-
console.log(`[error] cannot parse site.json: ${err.message}`);
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function writeSiteData(data) {
|
|
21
|
-
fs.writeFileSync(PATHS.siteData, `${JSON.stringify(data, null, 2)}\n`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function addSiteData(pageName) {
|
|
25
|
-
const data = readSiteData();
|
|
26
|
-
if (!data) return;
|
|
27
|
-
|
|
28
|
-
const camelName = toCamelCase(pageName);
|
|
29
|
-
if (data.pages[camelName]) {
|
|
30
|
-
console.log(`[skip] record "${camelName}" already exists`);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
data.pages[camelName] = {
|
|
35
|
-
seo: {
|
|
36
|
-
title: toNiceTitle(pageName),
|
|
37
|
-
description: 'description',
|
|
38
|
-
keywords: '',
|
|
39
|
-
noindex: false,
|
|
40
|
-
canonical: '',
|
|
41
|
-
},
|
|
42
|
-
cdn: { css: [], js: [] },
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
writeSiteData(data);
|
|
46
|
-
console.log(`[updated] record "${camelName}" added`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function removeSiteData(pageName) {
|
|
50
|
-
const data = readSiteData();
|
|
51
|
-
if (!data) return;
|
|
52
|
-
|
|
53
|
-
const camelName = toCamelCase(pageName);
|
|
54
|
-
if (!data.pages[camelName]) {
|
|
55
|
-
console.log(`[skip] record "${camelName}" not found`);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
delete data.pages[camelName];
|
|
60
|
-
writeSiteData(data);
|
|
61
|
-
console.log(`[cleaned] record "${camelName}" removed`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function renameSiteData(oldName, newName) {
|
|
65
|
-
const data = readSiteData();
|
|
66
|
-
if (!data) return;
|
|
67
|
-
|
|
68
|
-
const oldCamel = toCamelCase(oldName);
|
|
69
|
-
const newCamel = toCamelCase(newName);
|
|
70
|
-
|
|
71
|
-
if (!data.pages[oldCamel]) {
|
|
72
|
-
console.log(`[skip] record "${oldCamel}" not found`);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (data.pages[newCamel]) {
|
|
76
|
-
console.log(`[skip] record "${newCamel}" already exists`);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
data.pages[newCamel] = {
|
|
81
|
-
...data.pages[oldCamel],
|
|
82
|
-
seo: { ...data.pages[oldCamel].seo, title: toNiceTitle(newName) },
|
|
83
|
-
};
|
|
84
|
-
delete data.pages[oldCamel];
|
|
85
|
-
|
|
86
|
-
writeSiteData(data);
|
|
87
|
-
console.log(`[updated] record "${oldCamel}" renamed to "${newCamel}"`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
module.exports = { addSiteData, removeSiteData, renameSiteData };
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { PATHS } = require('./constants');
|
|
4
|
-
const { isTypeScriptProject } = require('./utils');
|
|
5
|
-
|
|
6
|
-
const OUTPUT_DIR_REGEX = /const OUTPUT_DIR\s*=\s*['"`]([^'"`]*)['"`]/;
|
|
7
|
-
const TSCONFIG_OUTDIR_REGEX = /"outDir"\s*:\s*"[^"]*"/;
|
|
8
|
-
|
|
9
|
-
function parseOutputDir(content) {
|
|
10
|
-
const match = content.match(OUTPUT_DIR_REGEX);
|
|
11
|
-
return match ? match[1] : null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function updateEleventyConfig(newPath) {
|
|
15
|
-
const content = fs.readFileSync(PATHS.eleventyConfig, 'utf8');
|
|
16
|
-
const updated = content.replace(OUTPUT_DIR_REGEX, `const OUTPUT_DIR = "${newPath}"`);
|
|
17
|
-
|
|
18
|
-
if (content === updated) {
|
|
19
|
-
console.log('[skip] OUTPUT_DIR not found in .eleventy.js');
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
fs.writeFileSync(PATHS.eleventyConfig, updated);
|
|
23
|
-
console.log(`[updated] .eleventy.js → ${newPath}`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function updatePackageJson(newPath) {
|
|
27
|
-
const pkg = JSON.parse(fs.readFileSync(PATHS.packageJson, 'utf8'));
|
|
28
|
-
const scriptGlob = isTypeScriptProject()
|
|
29
|
-
? 'src/frontend/ts/pages/*.ts'
|
|
30
|
-
: 'src/frontend/js/pages/*.js';
|
|
31
|
-
|
|
32
|
-
pkg.outputDir = newPath;
|
|
33
|
-
pkg.scripts = pkg.scripts || {};
|
|
34
|
-
pkg.scripts['build:css'] = `sass src/frontend/scss:${newPath}/css --no-source-map --style=compressed --quiet`;
|
|
35
|
-
pkg.scripts['serve:css'] = `sass --watch src/frontend/scss:${newPath}/css --no-source-map --quiet`;
|
|
36
|
-
pkg.scripts['build:js'] = `esbuild "${scriptGlob}" --bundle --outdir=${newPath}/js/pages --minify`;
|
|
37
|
-
pkg.scripts['serve:js'] = `esbuild "${scriptGlob}" --bundle --outdir=${newPath}/js/pages --watch`;
|
|
38
|
-
|
|
39
|
-
fs.writeFileSync(PATHS.packageJson, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
40
|
-
console.log(`[updated] package.json → ${newPath}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function updateTsConfig(newPath) {
|
|
44
|
-
if (!isTypeScriptProject()) return;
|
|
45
|
-
|
|
46
|
-
const content = fs.readFileSync(PATHS.tsconfig, 'utf8');
|
|
47
|
-
const updated = content.replace(TSCONFIG_OUTDIR_REGEX, `"outDir": "./${newPath}/ts"`);
|
|
48
|
-
|
|
49
|
-
if (content === updated) {
|
|
50
|
-
console.log('[skip] outDir not found in tsconfig.json');
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
fs.writeFileSync(PATHS.tsconfig, updated);
|
|
54
|
-
console.log(`[updated] tsconfig.json → ${newPath}/ts`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function deleteOldOutput(oldPath) {
|
|
58
|
-
if (!oldPath) return;
|
|
59
|
-
|
|
60
|
-
const oldAbsPath = path.resolve(PATHS.root, oldPath);
|
|
61
|
-
const insideProject = oldAbsPath.startsWith(PATHS.root + path.sep);
|
|
62
|
-
|
|
63
|
-
if (!insideProject || oldAbsPath === PATHS.root) {
|
|
64
|
-
console.log(`[skip] refusing to delete "${oldAbsPath}" (outside project)`);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (fs.existsSync(oldAbsPath)) {
|
|
68
|
-
fs.rmSync(oldAbsPath, { recursive: true, force: true });
|
|
69
|
-
console.log(`[deleted] ${oldAbsPath}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function updateOutputPath(rawPath) {
|
|
74
|
-
const trimmed = (rawPath ?? '').trim().replace(/\\/g, '/');
|
|
75
|
-
if (!trimmed) {
|
|
76
|
-
console.log('[skip] empty output path');
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const normalizedPath = trimmed === '.'
|
|
81
|
-
? 'out'
|
|
82
|
-
: `${trimmed.replace(/\/$/, '')}/${path.basename(PATHS.root)}-out`;
|
|
83
|
-
|
|
84
|
-
let oldPath = null;
|
|
85
|
-
try {
|
|
86
|
-
oldPath = parseOutputDir(fs.readFileSync(PATHS.eleventyConfig, 'utf8'));
|
|
87
|
-
} catch (err) {
|
|
88
|
-
console.log(`[error] cannot read .eleventy.js: ${err.message}`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
deleteOldOutput(oldPath);
|
|
93
|
-
|
|
94
|
-
console.log(`\nupdating output path → "${normalizedPath}"`);
|
|
95
|
-
try {
|
|
96
|
-
updatePackageJson(normalizedPath);
|
|
97
|
-
updateEleventyConfig(normalizedPath);
|
|
98
|
-
updateTsConfig(normalizedPath);
|
|
99
|
-
} catch (err) {
|
|
100
|
-
console.log(`[error] could not update output path: ${err.message}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function getCurrentOutputPath() {
|
|
105
|
-
try {
|
|
106
|
-
return parseOutputDir(fs.readFileSync(PATHS.eleventyConfig, 'utf8'));
|
|
107
|
-
} catch {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
module.exports = { updateOutputPath, getCurrentOutputPath };
|