create-berna-stencil 1.0.51 → 1.0.53

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 CHANGED
@@ -4,7 +4,7 @@ const Image = require("@11ty/eleventy-img");
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
6
 
7
- const OUTPUT_DIR = "out";
7
+ const OUTPUT_DIR = "c:/laragon/www/Berna-Stencil-out";
8
8
 
9
9
  module.exports = function (eleventyConfig) {
10
10
 
@@ -2,6 +2,8 @@ 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 ---
6
+
5
7
  const readerInterface = readline.createInterface({
6
8
  input: process.stdin,
7
9
  output: process.stdout,
@@ -10,86 +12,96 @@ const readerInterface = readline.createInterface({
10
12
 
11
13
  const PROTECTED_PAGES = ['homepage', '404'];
12
14
 
15
+ // --- Utility ---
16
+
17
+ // Converts any string to kebab-case
13
18
  function toKebabCase(str) {
14
19
  return str.trim().toLowerCase()
15
20
  .replace(/[\s_]+/g, '-')
16
21
  .replace(/-+/g, '-');
17
22
  }
18
23
 
19
- function handleCreateRequest() {
20
- readerInterface.question('\n> Enter the name of the new page: ', (inputName) => {
21
- const kebabName = toKebabCase(inputName);
22
- if (!kebabName) {
23
- console.log('(!) Invalid name.');
24
- } else if (/^\d/.test(kebabName)) {
25
- console.log('(!) Invalid name. Page name cannot start with a number.');
26
- } else if (PROTECTED_PAGES.includes(kebabName)) {
27
- console.log(`(!) "${kebabName}" is a protected page and cannot be created.`);
28
- } else {
29
- addPage(kebabName, null);
30
- }
31
- displayMainMenu();
32
- });
24
+ // Returns an error message if the name is invalid, null otherwise
25
+ 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.`;
29
+ return null;
30
+ }
31
+
32
+ // Wraps readerInterface.question in a Promise for use with async/await
33
+ function ask(prompt) {
34
+ return new Promise(resolve =>
35
+ readerInterface.question(prompt, answer => resolve(answer))
36
+ );
37
+ }
38
+
39
+ // Asks for a page name, converts it to kebab-case, validates it,
40
+ // logs the error and returns null if invalid
41
+ async function askPageName(prompt) {
42
+ const raw = await ask(prompt);
43
+ const name = toKebabCase(raw);
44
+ const error = validatePageName(name);
45
+ if (error) {
46
+ console.log(`(!) ${error}`);
47
+ return null;
48
+ }
49
+ return name;
50
+ }
51
+
52
+ // --- Handlers ---
53
+
54
+ async function handleCreateRequest() {
55
+ const name = await askPageName('\n> Enter the name of the new page: ');
56
+ if (name) addPage(name, null);
33
57
  }
34
58
 
35
- function handleRenameRequest() {
36
- readerInterface.question('\n> Enter the name of the page to rename: ', (inputOld) => {
37
- const oldName = toKebabCase(inputOld);
38
- if (!oldName) {
39
- console.log('(!) Invalid name.');
40
- return displayMainMenu();
41
- }
42
- if (PROTECTED_PAGES.includes(oldName)) {
43
- console.log(`(!) "${oldName}" is a protected page and cannot be renamed.`);
44
- return displayMainMenu();
45
- }
46
- readerInterface.question('> enter the new name: ', (inputNew) => {
47
- const newName = toKebabCase(inputNew);
48
- if (!newName) {
49
- console.log('(!) invalid name.');
50
- } else if (/^\d/.test(newName)) {
51
- console.log('(!) Invalid name. Page name cannot start with a number.');
52
- } else if (PROTECTED_PAGES.includes(newName)) {
53
- console.log(`(!) "${newName}" is a protected page name.`);
54
- } else if (oldName === newName) {
55
- console.log('(!) Old and new name are the same.');
56
- } else {
57
- renamePage(oldName, newName);
58
- }
59
- displayMainMenu();
60
- });
61
- });
59
+ async function handleRemoveRequest() {
60
+ const name = await askPageName('\n> Enter the name of the page to remove: ');
61
+ if (name) removePage(name);
62
62
  }
63
63
 
64
- function handleRemoveRequest() {
65
- readerInterface.question('\n> Enter the name of the page to remove: ', (inputName) => {
66
- const kebabName = toKebabCase(inputName);
67
- if (!kebabName) {
68
- console.log('(!) Invalid name.');
69
- } else if (PROTECTED_PAGES.includes(kebabName)) {
70
- console.log(`(!) "${kebabName}" Is a protected page and cannot be removed.`);
71
- } else {
72
- removePage(kebabName);
73
- }
74
- displayMainMenu();
75
- });
64
+ async function handleRenameRequest() {
65
+ const oldName = await askPageName('\n> Enter the name of the page to rename: ');
66
+ if (!oldName) return;
67
+
68
+ const newName = await askPageName('> Enter the new name: ');
69
+ if (!newName) return;
70
+
71
+ // Extra check: old and new name must differ
72
+ if (oldName === newName) {
73
+ console.log('(!) Old and new name are the same.');
74
+ return;
75
+ }
76
+
77
+ renamePage(oldName, newName);
76
78
  }
77
79
 
78
- function handleOutputPathRequest() {
80
+ async function handleOutputPathRequest() {
79
81
  const current = getCurrentOutputPath();
80
- const currentLabel = current ? ` Current path: "${current}"` : '';
81
-
82
- readerInterface.question(`${currentLabel}\n Enter the new output path: `, (inputPath) => {
83
- if (!inputPath.trim()) {
84
- console.log('(!) Invalid path.');
85
- } else {
86
- updateOutputPath(inputPath);
87
- }
88
- displayMainMenu();
89
- });
82
+ const label = current ? ` Current path: "${current}"\n` : '';
83
+ const input = await ask(`${label} Enter the new output path: `);
84
+
85
+ if (!input.trim()) {
86
+ console.log('(!) Invalid path.');
87
+ } else {
88
+ updateOutputPath(input);
89
+ }
90
90
  }
91
91
 
92
- function displayMainMenu() {
92
+ // --- Menu ---
93
+
94
+ // Maps each menu choice to its handler function
95
+ const MENU_ACTIONS = {
96
+ '1': handleCreateRequest,
97
+ '2': handleRemoveRequest,
98
+ '3': handleRenameRequest,
99
+ '4': handleOutputPathRequest,
100
+ };
101
+
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
+ async function displayMainMenu() {
93
105
  console.log('\n========================');
94
106
  console.log(' Berna-Stencil CLI ');
95
107
  console.log('========================\n');
@@ -99,24 +111,23 @@ function displayMainMenu() {
99
111
  console.log('4. Configure output path');
100
112
  console.log('\nCTRL/CMD + C to exit');
101
113
 
102
- readerInterface.question('\nChoose an option: ', (choice) => {
103
- const cleanChoice = choice.trim();
104
- if (cleanChoice === '1') {
105
- handleCreateRequest();
106
- } else if (cleanChoice === '2') {
107
- handleRemoveRequest();
108
- } else if (cleanChoice === '3') {
109
- handleRenameRequest();
110
- } else if (cleanChoice === '4') {
111
- handleOutputPathRequest();
112
- } else if (cleanChoice === '0') {
113
- readerInterface.close();
114
- process.exit(0);
115
- } else {
116
- console.log('(!) Invalid option.');
117
- displayMainMenu();
118
- }
119
- });
114
+ const choice = (await ask('\nChoose an option: ')).trim();
115
+
116
+ if (choice === '0') {
117
+ readerInterface.close();
118
+ process.exit(0);
119
+ }
120
+
121
+ const action = MENU_ACTIONS[choice];
122
+ if (action) {
123
+ await action();
124
+ } else {
125
+ console.log('(!) Invalid option.');
126
+ }
127
+
128
+ // Recurse to redisplay the menu after each action.
129
+ // Safe because each iteration fully resolves before the next one starts.
130
+ displayMainMenu();
120
131
  }
121
132
 
122
133
  displayMainMenu();
@@ -1,16 +1,46 @@
1
1
  const fileSystem = require('fs');
2
+
2
3
  const SITE_DATA_PATH = 'src/frontend/data/site.json';
3
4
 
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
4
10
  function toCamelCase(str) {
5
11
  return str.toLowerCase().replace(/[-_][a-z0-9]/g, (group) =>
6
- group.toUpperCase().replace('-', '').replace('_', '')
12
+ group.slice(1).toUpperCase()
7
13
  );
8
14
  }
9
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
+ // Returns the parsed site.json content, or null if the file doesn't exist
26
+ function readSiteData() {
27
+ if (!fileSystem.existsSync(SITE_DATA_PATH)) return null;
28
+ return JSON.parse(fileSystem.readFileSync(SITE_DATA_PATH, 'utf8'));
29
+ }
30
+
31
+ // Serializes and writes the data object back to site.json
32
+ function writeSiteData(data) {
33
+ fileSystem.writeFileSync(SITE_DATA_PATH, JSON.stringify(data, null, 2));
34
+ }
35
+
36
+ // --- Public API ---
37
+
38
+ // Adds a new page record to site.json
39
+ // Skips silently if the file doesn't exist or the record is already present
10
40
  function addSiteData(pageName) {
11
- if (!fileSystem.existsSync(SITE_DATA_PATH)) return;
41
+ const data = readSiteData();
42
+ if (!data) return;
12
43
 
13
- const data = JSON.parse(fileSystem.readFileSync(SITE_DATA_PATH, 'utf8'));
14
44
  const camelName = toCamelCase(pageName);
15
45
 
16
46
  if (data.pages[camelName]) {
@@ -18,15 +48,11 @@ function addSiteData(pageName) {
18
48
  return;
19
49
  }
20
50
 
21
- const niceTitle = pageName
22
- .split('-')
23
- .map(w => w.charAt(0).toUpperCase() + w.slice(1))
24
- .join(' ');
25
-
51
+ // Build the default page record with SEO metadata and empty CDN arrays
26
52
  data.pages[camelName] = {
27
53
  seo: {
28
- title: niceTitle,
29
- description: "description",
54
+ title: toNiceTitle(pageName),
55
+ description: 'description',
30
56
  },
31
57
  cdn: {
32
58
  css: [],
@@ -34,14 +60,16 @@ function addSiteData(pageName) {
34
60
  }
35
61
  };
36
62
 
37
- fileSystem.writeFileSync(SITE_DATA_PATH, JSON.stringify(data, null, 2));
63
+ writeSiteData(data);
38
64
  console.log(`[UPDATED] Record "${camelName}" added.`);
39
65
  }
40
66
 
67
+ // Removes a page record from site.json
68
+ // Skips silently if the file doesn't exist or the record is not found
41
69
  function removeSiteData(pageName) {
42
- if (!fileSystem.existsSync(SITE_DATA_PATH)) return;
70
+ const data = readSiteData();
71
+ if (!data) return;
43
72
 
44
- const data = JSON.parse(fileSystem.readFileSync(SITE_DATA_PATH, 'utf8'));
45
73
  const camelName = toCamelCase(pageName);
46
74
 
47
75
  if (!data.pages[camelName]) {
@@ -51,8 +79,44 @@ function removeSiteData(pageName) {
51
79
 
52
80
  delete data.pages[camelName];
53
81
 
54
- fileSystem.writeFileSync(SITE_DATA_PATH, JSON.stringify(data, null, 2));
82
+ writeSiteData(data);
55
83
  console.log(`[CLEANED] Record "${camelName}" removed.`);
56
84
  }
57
85
 
58
- module.exports = { addSiteData, removeSiteData };
86
+ // Renames a page record in site.json
87
+ // Preserves all existing fields (cdn, etc.) and only updates the SEO title
88
+ // Skips if the source record doesn't exist or the target name is already taken
89
+ function renameSiteData(oldName, newName) {
90
+ const data = readSiteData();
91
+ if (!data) return;
92
+
93
+ const oldCamel = toCamelCase(oldName);
94
+ const newCamel = toCamelCase(newName);
95
+
96
+ if (!data.pages[oldCamel]) {
97
+ console.log(`[SKIP] Record "${oldCamel}" not found.`);
98
+ return;
99
+ }
100
+
101
+ if (data.pages[newCamel]) {
102
+ console.log(`[SKIP] Record "${newCamel}" already exists.`);
103
+ return;
104
+ }
105
+
106
+ // Spread the existing record to preserve cdn and any future fields,
107
+ // then override only the seo.title with the new page name
108
+ data.pages[newCamel] = {
109
+ ...data.pages[oldCamel],
110
+ seo: {
111
+ ...data.pages[oldCamel].seo,
112
+ title: toNiceTitle(newName),
113
+ }
114
+ };
115
+
116
+ delete data.pages[oldCamel];
117
+
118
+ writeSiteData(data);
119
+ console.log(`[UPDATED] Record "${oldCamel}" renamed to "${newCamel}".`);
120
+ }
121
+
122
+ module.exports = { addSiteData, removeSiteData, renameSiteData };
@@ -1,45 +1,78 @@
1
1
  const fileSystem = require('fs');
2
+ const { toCamelCase } = require('./utils');
3
+
2
4
  const INCLUDES_PATH = 'src/frontend/components/layouts/includes.njk';
3
5
 
4
- function toCamelCase(str) {
5
- return str.toLowerCase().replace(/[-_][a-z0-9]/g, (group) =>
6
- group.toUpperCase().replace('-', '').replace('_', '')
6
+ // --- Helpers ---
7
+
8
+ // Returns the file content as a string, or null if the file doesn't exist
9
+ function readIncludes() {
10
+ if (!fileSystem.existsSync(INCLUDES_PATH)) return null;
11
+ return fileSystem.readFileSync(INCLUDES_PATH, 'utf8');
12
+ }
13
+
14
+ // Writes updated content back to the includes file
15
+ function writeIncludes(content) {
16
+ fileSystem.writeFileSync(INCLUDES_PATH, content);
17
+ }
18
+
19
+ // Builds the regex that matches the elif block for a given camelCase page name.
20
+ // Defined once here to avoid duplication and the stateful /g flag bug:
21
+ // using /g with .test() advances lastIndex, making a subsequent .replace() start
22
+ // from the wrong position. A fresh non-/g regex avoids this entirely.
23
+ function buildElifRegex(camelName) {
24
+ return new RegExp(
25
+ `[ \\t]*\\{%\\s*elif\\s+title\\s*==\\s*"${camelName}"\\s*%\\}[\\s\\S]*?(?=[ \\t]*\\{%\\s*(?:elif|else|endif))`,
7
26
  );
8
27
  }
9
28
 
29
+ // --- Public API ---
30
+
31
+ // Inserts a new elif block before {% else %} for the given page
10
32
  function addLayout(pageName) {
11
- const camelName = toCamelCase(pageName);
12
- if (!fileSystem.existsSync(INCLUDES_PATH)) return;
33
+ const content = readIncludes();
34
+ if (!content) return;
13
35
 
14
- let content = fileSystem.readFileSync(INCLUDES_PATH, 'utf8');
36
+ const camelName = toCamelCase(pageName);
15
37
 
38
+ // Skip if the block already exists
16
39
  if (content.includes(`{% elif title == "${camelName}" %}`)) return;
17
40
 
18
- const newElif = `{% elif title == "${camelName}" %}\n {# Insert your includes under this page #}\n {#{% include "component.njk" %}#}\n\n`;
19
- const updatedContent = content.replace('{% else %}', `${newElif}{% else %}`);
41
+ const newElif =
42
+ `{% elif title == "${camelName}" %}\n` +
43
+ ` {# Insert your includes under this page #}\n` +
44
+ ` {#{% include "component.njk" %}#}\n\n`;
20
45
 
21
- fileSystem.writeFileSync(INCLUDES_PATH, updatedContent);
46
+ writeIncludes(content.replace('{% else %}', `${newElif}{% else %}`));
22
47
  console.log(`[UPDATED] Layout block added for "${camelName}".`);
23
48
  }
24
49
 
50
+ // Removes the elif block for the given page, then collapses extra blank lines
25
51
  function removeLayout(pageName) {
26
- const camelName = toCamelCase(pageName);
27
- if (!fileSystem.existsSync(INCLUDES_PATH)) return;
28
-
29
- let content = fileSystem.readFileSync(INCLUDES_PATH, 'utf8');
52
+ const content = readIncludes();
53
+ if (!content) return;
30
54
 
31
- const regex = new RegExp(`[ \\t]*\\{%\\s*elif\\s+title\\s*==\\s*"${camelName}"\\s*%\\}[\\s\\S]*?(?=[ \\t]*\\{%\\s*(?:elif|else|endif))`, 'g');
55
+ const camelName = toCamelCase(pageName);
56
+ const regex = buildElifRegex(camelName);
32
57
 
33
58
  if (!regex.test(content)) {
34
59
  console.log(`[DEBUG] Layout block for "${camelName}" not found. Skipped.`);
35
60
  return;
36
61
  }
37
62
 
38
- let updatedContent = content.replace(regex, '');
39
- updatedContent = updatedContent.replace(/\n\s*\n\s*\n/g, '\n\n');
63
+ // Build a fresh regex instance for replace to avoid stale lastIndex
64
+ const updated = content
65
+ .replace(buildElifRegex(camelName), '')
66
+ .replace(/\n\s*\n\s*\n/g, '\n\n');
40
67
 
41
- fileSystem.writeFileSync(INCLUDES_PATH, updatedContent);
68
+ writeIncludes(updated);
42
69
  console.log(`[CLEANED] Layout block removed for "${camelName}".`);
43
70
  }
44
71
 
45
- module.exports = { addLayout, removeLayout };
72
+ // Renames a layout block by removing the old one and inserting a new one
73
+ function renameLayout(oldName, newName) {
74
+ removeLayout(oldName);
75
+ addLayout(newName);
76
+ }
77
+
78
+ module.exports = { addLayout, removeLayout, renameLayout };
@@ -4,11 +4,25 @@ const path = require('path');
4
4
  const ELEVENTY_CONFIG = path.resolve(__dirname, '../../.eleventy.js');
5
5
  const PACKAGE_JSON = path.resolve(__dirname, '../../package.json');
6
6
 
7
+ // Regex to locate the OUTPUT_DIR declaration in .eleventy.js.
8
+ // Extracted as a constant to avoid writing it by hand in multiple places.
9
+ const OUTPUT_DIR_REGEX = /const OUTPUT_DIR\s*=\s*['"`]([^'"`]*)['"`]/;
10
+
11
+ // --- Helpers ---
12
+
13
+ // Reads OUTPUT_DIR value from a given file content string, or returns null
14
+ function parseOutputDir(content) {
15
+ const match = content.match(OUTPUT_DIR_REGEX);
16
+ return match ? match[1] : null;
17
+ }
18
+
19
+ // --- Updaters ---
20
+
7
21
  function updateEleventyConfig(newPath) {
8
- let content = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
22
+ const content = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
9
23
 
10
24
  const updated = content.replace(
11
- /const OUTPUT_DIR\s*=\s*['"`][^'"`]*['"`]/,
25
+ OUTPUT_DIR_REGEX,
12
26
  `const OUTPUT_DIR = "${newPath}"`
13
27
  );
14
28
 
@@ -23,34 +37,37 @@ function updateEleventyConfig(newPath) {
23
37
  }
24
38
 
25
39
  function updatePackageJson(newPath) {
26
- const raw = fs.readFileSync(PACKAGE_JSON, 'utf-8');
27
- const pkg = JSON.parse(raw);
40
+ const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf-8'));
28
41
 
42
+ // Reconstruct all output-dependent scripts from scratch to avoid
43
+ // partial string replacement bugs on the outdir flag
29
44
  pkg.scripts['build:css'] = `sass src/frontend/scss:${newPath}/css --no-source-map --style=compressed --quiet`;
30
- pkg.scripts['build:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --minify`;
45
+ pkg.scripts['build:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --minify`;
31
46
  pkg.scripts['serve:css'] = `sass --watch src/frontend/scss:${newPath}/css --no-source-map --quiet`;
32
- pkg.scripts['serve:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --watch`;
47
+ pkg.scripts['serve:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --watch`;
33
48
 
34
49
  fs.writeFileSync(PACKAGE_JSON, JSON.stringify(pkg, null, 2), 'utf-8');
35
50
  console.log(`(✓) package.json updated → ${newPath}`);
36
51
  return true;
37
52
  }
38
53
 
54
+ // --- Public API ---
55
+
39
56
  function updateOutputPath(newPath) {
40
57
  const trimmed = newPath.trim().replace(/\\/g, '/');
41
58
 
42
- let normalizedPath;
43
- if (trimmed === '.') {
44
- normalizedPath = 'out';
45
- } else {
46
- const projectName = path.basename(process.cwd());
47
- normalizedPath = trimmed.replace(/\/$/, '') + '/' + projectName + '-out';
48
- }
59
+ // Normalize the path: bare "." becomes "out", everything else gets a
60
+ // project-scoped suffix to avoid collisions
61
+ const normalizedPath = trimmed === '.'
62
+ ? 'out'
63
+ : `${trimmed.replace(/\/$/, '')}/${path.basename(process.cwd())}-out`;
49
64
 
65
+ // Read the config once and reuse it to get the old path —
66
+ // avoids a second disk read inside updateEleventyConfig
50
67
  const eleventyContent = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
51
- const match = eleventyContent.match(/const OUTPUT_DIR\s*=\s*['"`]([^'"`]*)['"`]/);
52
- const oldPath = match ? match[1] : null;
68
+ const oldPath = parseOutputDir(eleventyContent);
53
69
 
70
+ // Delete the old output folder if it exists
54
71
  if (oldPath) {
55
72
  const oldAbsPath = path.resolve(__dirname, '../../', oldPath);
56
73
  if (fs.existsSync(oldAbsPath)) {
@@ -67,19 +84,11 @@ function updateOutputPath(newPath) {
67
84
  updateEleventyConfig(normalizedPath);
68
85
  }
69
86
 
87
+ // Returns the current OUTPUT_DIR value from .eleventy.js, or null on failure
70
88
  function getCurrentOutputPath() {
71
89
  try {
72
90
  const content = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
73
- const match = content.match(/const OUTPUT_DIR\s*=\s*['"`]([^'"`]*)['"`]/);
74
- if (!match) return null;
75
-
76
- const outputDir = match[1];
77
-
78
-
79
- const parent = path.dirname(outputDir);
80
- const projectName = path.basename(outputDir);
81
-
82
- return `${parent}/${projectName}`;
91
+ return parseOutputDir(content);
83
92
  } catch {
84
93
  return null;
85
94
  }