create-berna-stencil 2.2.1 → 2.3.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/README.md +1 -1
- package/_tools/assistant.js +58 -45
- package/_tools/modules/updateData.js +1 -20
- package/_tools/modules/updateOutputPath.js +2 -5
- package/_tools/modules/updatePage.js +13 -15
- package/_tools/modules/utils.js +19 -1
- package/bin/create.js +1 -1
- package/package.json +6 -6
- package/src/frontend/ts/pages/404.ts +4 -4
- package/src/frontend/ts/pages/homepage.ts +4 -4
- package/tsconfig.json +2 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Building a website from scratch involves a lot of moving parts: templating engin
|
|
|
12
12
|
- 📁 **Scalable structure** — a clean, opinionated project layout that grows with your needs
|
|
13
13
|
- 🌍 **Open source** — free to use, free to modify, free to share
|
|
14
14
|
|
|
15
|
-

|
|
16
16
|

|
|
17
17
|

|
|
18
18
|
|
package/_tools/assistant.js
CHANGED
|
@@ -2,7 +2,16 @@ const readline = require('readline');
|
|
|
2
2
|
const { addPage, removePage, renamePage } = require('./modules/updatePage');
|
|
3
3
|
const { updateOutputPath, getCurrentOutputPath } = require('./modules/updateOutputPath');
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const c = {
|
|
6
|
+
reset: "\x1b[0m",
|
|
7
|
+
bold: "\x1b[1m",
|
|
8
|
+
dim: "\x1b[2m",
|
|
9
|
+
red: "\x1b[31m",
|
|
10
|
+
green: "\x1b[32m",
|
|
11
|
+
yellow: "\x1b[33m",
|
|
12
|
+
magenta: "\x1b[35m",
|
|
13
|
+
cyan: "\x1b[36m"
|
|
14
|
+
};
|
|
6
15
|
|
|
7
16
|
const readerInterface = readline.createInterface({
|
|
8
17
|
input: process.stdin,
|
|
@@ -10,67 +19,73 @@ const readerInterface = readline.createInterface({
|
|
|
10
19
|
terminal: true
|
|
11
20
|
});
|
|
12
21
|
|
|
13
|
-
const PROTECTED_PAGES
|
|
14
|
-
|
|
15
|
-
// --- Utility ---
|
|
22
|
+
const PROTECTED_PAGES = ['homepage', '404'];
|
|
23
|
+
const MAX_NAME_LENGTH = 50;
|
|
16
24
|
|
|
17
|
-
// Converts any string to kebab-case
|
|
18
25
|
function toKebabCase(str) {
|
|
19
26
|
return str.trim().toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9\s_-]/g, '')
|
|
20
28
|
.replace(/[\s_]+/g, '-')
|
|
21
|
-
.replace(/-+/g, '-')
|
|
29
|
+
.replace(/-+/g, '-')
|
|
30
|
+
.replace(/^-+|-+$/g, '');
|
|
22
31
|
}
|
|
23
32
|
|
|
24
|
-
// Returns an error message if the name is invalid, null otherwise
|
|
25
33
|
function validatePageName(name) {
|
|
26
|
-
if (!name)
|
|
27
|
-
if (
|
|
28
|
-
if (
|
|
34
|
+
if (!name) return 'Invalid name.';
|
|
35
|
+
if (name.length > MAX_NAME_LENGTH) return `Name must be ${MAX_NAME_LENGTH} characters or fewer.`;
|
|
36
|
+
if (!/^[a-z0-9-]+$/.test(name)) return 'Page name can only contain lowercase letters, numbers, and hyphens.';
|
|
37
|
+
if (/^\d/.test(name)) return 'Page name cannot start with a number.';
|
|
38
|
+
if (PROTECTED_PAGES.includes(name)) return `"${name}" is a protected page name.`;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validateOutputPath(input) {
|
|
43
|
+
if (!input.trim()) return 'Invalid path.';
|
|
44
|
+
if (input.includes('..')) return 'Path cannot contain "..".';
|
|
45
|
+
if (/[<>|?*"']/.test(input)) return 'Path contains invalid characters.';
|
|
29
46
|
return null;
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
|
|
49
|
+
function sanitizeInput(str) {
|
|
50
|
+
return str.replace(/[\x00-\x1F\x7F]/g, '').trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
function ask(prompt) {
|
|
34
54
|
return new Promise(resolve =>
|
|
35
|
-
readerInterface.question(prompt, answer => resolve(answer))
|
|
55
|
+
readerInterface.question(prompt, answer => resolve(sanitizeInput(answer)))
|
|
36
56
|
);
|
|
37
57
|
}
|
|
38
58
|
|
|
39
|
-
// Asks for a page name, converts it to kebab-case, validates it,
|
|
40
|
-
// logs the error and returns null if invalid
|
|
41
59
|
async function askPageName(prompt) {
|
|
42
60
|
const raw = await ask(prompt);
|
|
43
61
|
const name = toKebabCase(raw);
|
|
44
62
|
const error = validatePageName(name);
|
|
45
63
|
if (error) {
|
|
46
|
-
console.log(
|
|
64
|
+
console.log(`\n${c.red}✖ ${error}${c.reset}`);
|
|
47
65
|
return null;
|
|
48
66
|
}
|
|
49
67
|
return name;
|
|
50
68
|
}
|
|
51
69
|
|
|
52
|
-
// --- Handlers ---
|
|
53
|
-
|
|
54
70
|
async function handleCreateRequest() {
|
|
55
|
-
const name = await askPageName(
|
|
71
|
+
const name = await askPageName(`\n${c.green}❯${c.reset} Enter the name of the new page: `);
|
|
56
72
|
if (name) addPage(name, null);
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
async function handleRemoveRequest() {
|
|
60
|
-
const name = await askPageName(
|
|
76
|
+
const name = await askPageName(`\n${c.red}❯${c.reset} Enter the name of the page to remove: `);
|
|
61
77
|
if (name) removePage(name);
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
async function handleRenameRequest() {
|
|
65
|
-
const oldName = await askPageName(
|
|
81
|
+
const oldName = await askPageName(`\n${c.yellow}❯${c.reset} Enter the name of the page to rename: `);
|
|
66
82
|
if (!oldName) return;
|
|
67
83
|
|
|
68
|
-
const newName = await askPageName(
|
|
84
|
+
const newName = await askPageName(`${c.yellow}❯${c.reset} Enter the new name: `);
|
|
69
85
|
if (!newName) return;
|
|
70
86
|
|
|
71
|
-
// Extra check: old and new name must differ
|
|
72
87
|
if (oldName === newName) {
|
|
73
|
-
console.log(
|
|
88
|
+
console.log(`\n${c.yellow}⚠ Old and new name are the same.${c.reset}`);
|
|
74
89
|
return;
|
|
75
90
|
}
|
|
76
91
|
|
|
@@ -79,19 +94,17 @@ async function handleRenameRequest() {
|
|
|
79
94
|
|
|
80
95
|
async function handleOutputPathRequest() {
|
|
81
96
|
const current = getCurrentOutputPath();
|
|
82
|
-
const label
|
|
83
|
-
const input
|
|
97
|
+
const label = current ? `\n${c.dim}Current path: "${current}"${c.reset}\n` : '\n';
|
|
98
|
+
const input = await ask(`${label}${c.magenta}❯${c.reset} Enter the new output path: `);
|
|
84
99
|
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
const error = validateOutputPath(input);
|
|
101
|
+
if (error) {
|
|
102
|
+
console.log(`\n${c.red}✖ ${error}${c.reset}`);
|
|
87
103
|
} else {
|
|
88
104
|
updateOutputPath(input);
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
107
|
|
|
92
|
-
// --- Menu ---
|
|
93
|
-
|
|
94
|
-
// Maps each menu choice to its handler function
|
|
95
108
|
const MENU_ACTIONS = {
|
|
96
109
|
'1': handleCreateRequest,
|
|
97
110
|
'2': handleRemoveRequest,
|
|
@@ -99,19 +112,17 @@ const MENU_ACTIONS = {
|
|
|
99
112
|
'4': handleOutputPathRequest,
|
|
100
113
|
};
|
|
101
114
|
|
|
102
|
-
// Displays the menu, waits for input, executes the chosen action,
|
|
103
|
-
// then calls itself again to keep the CLI alive (async recursion, no stack buildup)
|
|
104
115
|
async function displayMainMenu() {
|
|
105
|
-
console.log(
|
|
106
|
-
console.log(
|
|
107
|
-
console.log(
|
|
108
|
-
console.log(
|
|
109
|
-
console.log(
|
|
110
|
-
console.log(
|
|
111
|
-
console.log(
|
|
112
|
-
console.log(
|
|
116
|
+
console.log(`\n${c.cyan}${c.bold}╭────────────────────────╮`);
|
|
117
|
+
console.log(`│ Berna-Stencil CLI │`);
|
|
118
|
+
console.log(`╰────────────────────────╯${c.reset}\n`);
|
|
119
|
+
console.log(` ${c.green}1.${c.reset} Create page`);
|
|
120
|
+
console.log(` ${c.red}2.${c.reset} Remove page`);
|
|
121
|
+
console.log(` ${c.yellow}3.${c.reset} Rename page`);
|
|
122
|
+
console.log(` ${c.magenta}4.${c.reset} Configure output path`);
|
|
123
|
+
console.log(`\n ${c.dim}CTRL/CMD + C to exit${c.reset}\n`);
|
|
113
124
|
|
|
114
|
-
const choice = (await ask(
|
|
125
|
+
const choice = (await ask(`${c.cyan}❯${c.reset} Choose an option: `)).trim();
|
|
115
126
|
|
|
116
127
|
if (choice === '0') {
|
|
117
128
|
readerInterface.close();
|
|
@@ -120,13 +131,15 @@ async function displayMainMenu() {
|
|
|
120
131
|
|
|
121
132
|
const action = MENU_ACTIONS[choice];
|
|
122
133
|
if (action) {
|
|
123
|
-
|
|
134
|
+
try {
|
|
135
|
+
await action();
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.log(`\n${c.red}✖ Unexpected error: ${err.message}${c.reset}`);
|
|
138
|
+
}
|
|
124
139
|
} else {
|
|
125
|
-
console.log(
|
|
140
|
+
console.log(`\n${c.red}✖ Invalid option.${c.reset}`);
|
|
126
141
|
}
|
|
127
142
|
|
|
128
|
-
// Recurse to redisplay the menu after each action.
|
|
129
|
-
// Safe because each iteration fully resolves before the next one starts.
|
|
130
143
|
displayMainMenu();
|
|
131
144
|
}
|
|
132
145
|
|
|
@@ -1,27 +1,8 @@
|
|
|
1
1
|
const fileSystem = require('fs');
|
|
2
|
+
const { toCamelCase, toNiceTitle } = require('./utils');
|
|
2
3
|
|
|
3
4
|
const SITE_DATA_PATH = 'src/frontend/data/site.json';
|
|
4
5
|
|
|
5
|
-
// --- Utility ---
|
|
6
|
-
|
|
7
|
-
// Converts a kebab-case string to camelCase
|
|
8
|
-
// slice(1) removes the delimiter character reliably, avoiding the regex flag bug
|
|
9
|
-
// of a double replace('-', '') without the /g flag
|
|
10
|
-
function toCamelCase(str) {
|
|
11
|
-
return str.toLowerCase().replace(/[-_][a-z0-9]/g, (group) =>
|
|
12
|
-
group.slice(1).toUpperCase()
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Converts a kebab-case page name to a human-readable title
|
|
17
|
-
// e.g. "about-us" → "About Us"
|
|
18
|
-
function toNiceTitle(pageName) {
|
|
19
|
-
return pageName
|
|
20
|
-
.split('-')
|
|
21
|
-
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
22
|
-
.join(' ');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
6
|
// Returns the parsed site.json content, or null if the file doesn't exist
|
|
26
7
|
function readSiteData() {
|
|
27
8
|
if (!fileSystem.existsSync(SITE_DATA_PATH)) return null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const fs
|
|
1
|
+
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { isTypeScriptProject } = require('./utils');
|
|
3
4
|
|
|
4
5
|
const ELEVENTY_CONFIG = path.resolve(__dirname, '../../.eleventy.js');
|
|
5
6
|
const PACKAGE_JSON = path.resolve(__dirname, '../../package.json');
|
|
@@ -14,10 +15,6 @@ function parseOutputDir(content) {
|
|
|
14
15
|
return match ? match[1] : null;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
function isTypeScriptProject() {
|
|
18
|
-
return fs.existsSync(TSCONFIG);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
18
|
// --- Updaters ---
|
|
22
19
|
|
|
23
20
|
function updateEleventyConfig(newPath) {
|
|
@@ -4,18 +4,10 @@ const path = require('path');
|
|
|
4
4
|
const { addSiteData, removeSiteData, renameSiteData } = require('./updateData');
|
|
5
5
|
const { addLayout, removeLayout, renameLayout } = require('./updateIncludes');
|
|
6
6
|
const { getCurrentOutputPath } = require('./updateOutputPath');
|
|
7
|
-
const { toCamelCase } = require('./utils');
|
|
7
|
+
const { toCamelCase, isTypeScriptProject } = require('./utils');
|
|
8
8
|
|
|
9
9
|
const TEMPLATES_DIR = path.join(__dirname, '..', 'res', 'templates');
|
|
10
|
-
const TSCONFIG = path.resolve(__dirname, '../../tsconfig.json');
|
|
11
10
|
|
|
12
|
-
// --- Helpers ---
|
|
13
|
-
|
|
14
|
-
function isTypeScriptProject() {
|
|
15
|
-
return fileSystem.existsSync(TSCONFIG);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Returns the three file targets (scss, js/ts, njk) for a given page name
|
|
19
11
|
function getPageTargets(pageName) {
|
|
20
12
|
const camelName = toCamelCase(pageName);
|
|
21
13
|
const usesTs = isTypeScriptProject();
|
|
@@ -39,8 +31,6 @@ function getPageTargets(pageName) {
|
|
|
39
31
|
];
|
|
40
32
|
}
|
|
41
33
|
|
|
42
|
-
// --- Public API ---
|
|
43
|
-
|
|
44
34
|
function addPage(pageName) {
|
|
45
35
|
const camelName = toCamelCase(pageName);
|
|
46
36
|
|
|
@@ -86,8 +76,16 @@ function renamePage(oldName, newName) {
|
|
|
86
76
|
console.log(`[skip] not found: ${src}`);
|
|
87
77
|
return;
|
|
88
78
|
}
|
|
79
|
+
|
|
89
80
|
fileSystem.renameSync(src, dest);
|
|
90
81
|
console.log(`[renamed] ${src} → ${dest}`);
|
|
82
|
+
|
|
83
|
+
if (dest.endsWith('.njk')) {
|
|
84
|
+
const content = fileSystem.readFileSync(dest, 'utf8')
|
|
85
|
+
.replace(/^title:.*$/m, `title: "${newCamel}"`)
|
|
86
|
+
.replace(/^permalink:.*$/m, `permalink: "/${newName}/"`);
|
|
87
|
+
fileSystem.writeFileSync(dest, content);
|
|
88
|
+
}
|
|
91
89
|
});
|
|
92
90
|
|
|
93
91
|
renameLayout(oldName, newName);
|
|
@@ -95,10 +93,10 @@ function renamePage(oldName, newName) {
|
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
function removePage(pageName) {
|
|
98
|
-
const camelName
|
|
99
|
-
const usesTs
|
|
100
|
-
const ext
|
|
101
|
-
const jsFolder
|
|
96
|
+
const camelName = toCamelCase(pageName);
|
|
97
|
+
const usesTs = isTypeScriptProject();
|
|
98
|
+
const ext = usesTs ? 'ts' : 'js';
|
|
99
|
+
const jsFolder = usesTs ? 'src/frontend/ts/pages' : 'src/frontend/js/pages';
|
|
102
100
|
const OUTPUT_DIR = getCurrentOutputPath() || 'out';
|
|
103
101
|
|
|
104
102
|
const filesToDelete = [
|
package/_tools/modules/utils.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const TSCONFIG = path.resolve(__dirname, '../../tsconfig.json');
|
|
5
|
+
|
|
1
6
|
// Shared utility functions used across modules
|
|
2
7
|
|
|
3
8
|
// Converts a kebab-case or snake_case string to camelCase.
|
|
@@ -9,4 +14,17 @@ function toCamelCase(str) {
|
|
|
9
14
|
);
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
// Converts a kebab-case page name to a human-readable title
|
|
18
|
+
// e.g. "about-us" → "About Us"
|
|
19
|
+
function toNiceTitle(pageName) {
|
|
20
|
+
return pageName
|
|
21
|
+
.split('-')
|
|
22
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
23
|
+
.join(' ');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isTypeScriptProject() {
|
|
27
|
+
return fs.existsSync(TSCONFIG);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { toCamelCase, toNiceTitle, isTypeScriptProject };
|
package/bin/create.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-berna-stencil",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Eleventy boilerplate with per-page SCSS/JS pipeline, esbuild bundling, multi-framework CSS support and a built-in page management CLI",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"author": "Michele Garofalo",
|
|
@@ -48,13 +48,13 @@
|
|
|
48
48
|
"esbuild": "^0.27.3",
|
|
49
49
|
"sass": "^1.77.0"
|
|
50
50
|
},
|
|
51
|
-
"scripts": {
|
|
52
|
-
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet
|
|
53
|
-
"build:js": "
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet",
|
|
53
|
+
"build:js": "esbuild \"src/frontend/ts/pages/*.ts\" --bundle --outdir=out/js/pages --minify",
|
|
54
54
|
"build:11ty": "eleventy",
|
|
55
55
|
"build": "npm run clean && npm run build:css && npm run build:js && npm run build:11ty",
|
|
56
|
-
"serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet
|
|
57
|
-
"serve:js": "
|
|
56
|
+
"serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet",
|
|
57
|
+
"serve:js": "esbuild \"src/frontend/ts/pages/*.ts\" --bundle --outdir=out/js/pages --watch",
|
|
58
58
|
"serve:11ty": "eleventy --serve --quiet",
|
|
59
59
|
"clean": "node _tools/cleanOutput.js",
|
|
60
60
|
"serve": "npm run clean && concurrently \"npm run serve:11ty\" \"npm run serve:css\" \"npm run serve:js\"",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
//===========================
|
|
2
2
|
// TYPESCRIPT MODULES IMPORTS
|
|
3
|
-
|
|
3
|
+
//===========================
|
|
4
4
|
|
|
5
5
|
// import { initExampleModule } from '../modules/exampleModule';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
//==========================
|
|
8
8
|
// PAGE CUSTOM TYPESCRIPT
|
|
9
|
-
|
|
9
|
+
//==========================
|
|
10
10
|
|
|
11
11
|
document.addEventListener("DOMContentLoaded", (): void => {
|
|
12
12
|
// initExampleModule();
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
//===========================
|
|
2
2
|
// TYPESCRIPT MODULES IMPORTS
|
|
3
|
-
|
|
3
|
+
//===========================
|
|
4
4
|
|
|
5
5
|
// import { initExampleModule } from '../modules/exampleModule';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
//==========================
|
|
8
8
|
// PAGE CUSTOM TYPESCRIPT
|
|
9
|
-
|
|
9
|
+
//==========================
|
|
10
10
|
|
|
11
11
|
document.addEventListener("DOMContentLoaded", (): void => {
|
|
12
12
|
// initExampleModule();
|