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 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
- ![Version](https://img.shields.io/badge/version-2.2.1-blue)
15
+ ![Version](https://img.shields.io/badge/version-2.3.0-blue)
16
16
  ![License](https://img.shields.io/badge/license-Apache--2.0-blue)
17
17
  ![Eleventy](https://img.shields.io/badge/11ty-v3.1.2-black)
18
18
 
@@ -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
- // --- Setup ---
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 = ['homepage', '404'];
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) return 'Invalid name.';
27
- if (/^\d/.test(name)) return 'Page name cannot start with a number.';
28
- if (PROTECTED_PAGES.includes(name)) return `"${name}" is a protected page name.`;
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
- // Wraps readerInterface.question in a Promise for use with async/await
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(`(!) ${error}`);
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('\n> Enter the name of the new page: ');
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('\n> Enter the name of the page to remove: ');
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('\n> Enter the name of the page to rename: ');
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('> Enter the new name: ');
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('(!) Old and new name are the same.');
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 = current ? ` Current path: "${current}"\n` : '';
83
- const input = await ask(`${label} Enter the new output path: `);
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
- if (!input.trim()) {
86
- console.log('(!) Invalid path.');
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('\n========================');
106
- console.log(' Berna-Stencil CLI ');
107
- console.log('========================\n');
108
- console.log('1. Create page');
109
- console.log('2. Remove page');
110
- console.log('3. Rename page');
111
- console.log('4. Configure output path');
112
- console.log('\nCTRL/CMD + C to exit');
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('\nChoose an option: ')).trim();
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
- await action();
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('(!) Invalid option.');
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 = require('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 = toCamelCase(pageName);
99
- const usesTs = isTypeScriptProject();
100
- const ext = usesTs ? 'ts' : 'js';
101
- const jsFolder = usesTs ? 'src/frontend/ts/pages' : 'src/frontend/js/pages';
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 = [
@@ -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
- module.exports = { toCamelCase };
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
@@ -120,7 +120,7 @@ src/backend/config.php
120
120
 
121
121
  const PROJECT_PACKAGE = {
122
122
  name: path.basename(targetDir),
123
- version: '2.2.1',
123
+ version: '2.3.0',
124
124
  private: true,
125
125
  outputDir: 'out',
126
126
  "scripts": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-berna-stencil",
3
- "version": "2.2.1",
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 --load-path=node_modules",
53
- "build:js": "node _tools/buildJs.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 --load-path=node_modules",
57
- "serve:js": "node _tools/buildJs.js --watch",
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();
package/tsconfig.json CHANGED
@@ -19,6 +19,7 @@
19
19
  "isolatedModules": true,
20
20
  "noUncheckedSideEffectImports": true,
21
21
  "moduleDetection": "force",
22
- "skipLibCheck": true
22
+ "skipLibCheck": true,
23
+ "experimentalDecorators": true
23
24
  }
24
25
  }