create-berna-stencil 2.0.4 → 2.0.6

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.
@@ -1,11 +1,11 @@
1
- //==========================
1
+ //===========================
2
2
  // JAVASCRIPT MODULES IMPORTS
3
- //==========================
3
+ //===========================
4
4
 
5
5
  // Call anywhere
6
6
  import { showNotification } from '../modules/notification.js';
7
7
 
8
- // Uncomment to enable optional modules (call inside DOMContentLoaded)
8
+ // Uncomment of pre-existing modules
9
9
  // import { initTextAreaAutoExpand } from '../modules/forms/textAreaAutoExpand.js';
10
10
  // import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
11
11
 
@@ -14,6 +14,8 @@ import { showNotification } from '../modules/notification.js';
14
14
  //==========================
15
15
 
16
16
  document.addEventListener("DOMContentLoaded", () => {
17
+ // initTextAreaAutoExpand();
18
+ // initNormalizePhoneNumber();
17
19
  });
18
20
 
19
21
  showNotification("Example notification", "success", 3000);
package/bin/create.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const readline = require('readline');
5
6
 
6
7
  const targetDir = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
7
8
  const templateDir = path.join(__dirname, '..');
@@ -15,7 +16,7 @@ const COPY_TARGETS = [
15
16
 
16
17
  const PROJECT_PACKAGE = {
17
18
  name: path.basename(targetDir),
18
- version: '2.0.4',
19
+ version: '2.0.6',
19
20
  private: true,
20
21
  scripts: {
21
22
  "build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet --load-path=node_modules",
@@ -54,12 +55,63 @@ out/
54
55
  src/backend/config.php
55
56
  `;
56
57
 
58
+ // Framework configurations
59
+ const FRAMEWORKS = {
60
+ bootstrap: {
61
+ label: 'Bootstrap',
62
+ scss: 'bootstrap',
63
+ njk: [
64
+ '<script src="/js/bootstrap.bundle.min.js" defer></script>',
65
+ ],
66
+ eleventy: [
67
+ '"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js": "js/bootstrap.bundle.min.js",',
68
+ '"node_modules/bootstrap-icons/font/fonts": "css/fonts",',
69
+ ],
70
+ },
71
+ bulma: {
72
+ label: 'Bulma',
73
+ scss: 'bulma',
74
+ njk: [],
75
+ eleventy: [],
76
+ },
77
+ foundation: {
78
+ label: 'Foundation',
79
+ scss: 'foundation',
80
+ njk: ['<script src="/js/foundation.min.js" defer></script>'],
81
+ eleventy: ['"node_modules/foundation-sites/dist/js/foundation.min.js": "js/foundation.min.js",'],
82
+ },
83
+ uikit: {
84
+ label: 'UIkit',
85
+ scss: 'uikit',
86
+ njk: [
87
+ '<script src="/js/uikit.min.js" defer></script>',
88
+ '<script src="/js/uikit-icons.min.js" defer></script>',
89
+ ],
90
+ eleventy: [
91
+ '"node_modules/uikit/dist/js/uikit.min.js": "js/uikit.min.js",',
92
+ '"node_modules/uikit/dist/js/uikit-icons.min.js": "js/uikit-icons.min.js",',
93
+ ],
94
+ },
95
+ none: {
96
+ label: 'None',
97
+ scss: null,
98
+ njk: [],
99
+ eleventy: [],
100
+ },
101
+ };
102
+
103
+ const ALL_FRAMEWORKS = ['bootstrap', 'bulma', 'foundation', 'uikit'];
104
+
57
105
  const { writeSync } = require('fs');
58
106
 
59
107
  function log(msg) {
60
108
  writeSync(1, msg + '\n');
61
109
  }
62
110
 
111
+ function escapeRegex(str) {
112
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
113
+ }
114
+
63
115
  function copyRecursive(src, dest) {
64
116
  const stat = fs.statSync(src);
65
117
  if (stat.isDirectory()) {
@@ -85,45 +137,171 @@ function deleteFileRecursive(dir, filename) {
85
137
  }
86
138
  }
87
139
 
88
- if (!fs.existsSync(targetDir)) {
89
- fs.mkdirSync(targetDir, { recursive: true });
140
+ function slashComment(content, line) {
141
+ content = content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
142
+ return content.replace(new RegExp(`^([ \\t]*)(${escapeRegex(line)})$`, 'gm'), '$1// $2');
90
143
  }
91
144
 
92
- log(`\n>> Creating berna-stencil project in ${targetDir}\n`);
145
+ function slashUncomment(content, line) {
146
+ return content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
147
+ }
148
+
149
+ function njkComment(content, line) {
150
+ content = content.split(`{# ${line} #}`).join(line);
151
+ return content.split(line).join(`{# ${line} #}`);
152
+ }
153
+
154
+ function njkUncomment(content, line) {
155
+ return content.split(`{# ${line} #}`).join(line);
156
+ }
157
+
158
+ function applyFramework(framework) {
159
+ const config = FRAMEWORKS[framework];
160
+
161
+ const globalScssPath = path.join(targetDir, 'src/frontend/scss/modules/_global.scss');
162
+ if (fs.existsSync(globalScssPath)) {
163
+ let content = fs.readFileSync(globalScssPath, 'utf8');
164
+ ALL_FRAMEWORKS.forEach(fw => {
165
+ content = slashComment(content, `@import "../modules/frameworks/${fw}";`);
166
+ });
167
+ if (config.scss) {
168
+ content = slashUncomment(content, `@import "../modules/frameworks/${config.scss}";`);
169
+ }
170
+ fs.writeFileSync(globalScssPath, content);
171
+ }
172
+
173
+ const baseNjkPath = path.join(targetDir, 'src/frontend/components/layouts/base.njk');
174
+ if (fs.existsSync(baseNjkPath)) {
175
+ let content = fs.readFileSync(baseNjkPath, 'utf8');
176
+ ALL_FRAMEWORKS.forEach(fw => {
177
+ FRAMEWORKS[fw].njk.forEach(line => {
178
+ content = njkComment(content, line);
179
+ });
180
+ });
181
+ config.njk.forEach(line => {
182
+ content = njkUncomment(content, line);
183
+ });
184
+ fs.writeFileSync(baseNjkPath, content);
185
+ }
93
186
 
94
- for (const target of COPY_TARGETS) {
95
- const src = path.join(templateDir, target);
96
- const dest = path.join(targetDir, target);
97
- if (fs.existsSync(src)) {
98
- copyRecursive(src, dest);
99
- log(`+ ${target}`);
187
+ const eleventyPath = path.join(targetDir, '.eleventy.js');
188
+ if (fs.existsSync(eleventyPath)) {
189
+ let content = fs.readFileSync(eleventyPath, 'utf8');
190
+ ALL_FRAMEWORKS.forEach(fw => {
191
+ FRAMEWORKS[fw].eleventy.forEach(line => {
192
+ content = slashComment(content, line);
193
+ });
194
+ });
195
+ config.eleventy.forEach(line => {
196
+ content = slashUncomment(content, line);
197
+ });
198
+ fs.writeFileSync(eleventyPath, content);
100
199
  }
101
200
  }
102
201
 
103
- // config.php
104
- const configDest = path.join(targetDir, 'src/backend/config.php');
105
- const configExample = path.join(targetDir, 'src/backend/config.example.php');
106
- if (!fs.existsSync(configDest) && fs.existsSync(configExample)) {
107
- fs.copyFileSync(configExample, configDest);
108
- log('+ src/backend/config.php');
202
+ function askFramework() {
203
+ return new Promise((resolve) => {
204
+ const choices = [
205
+ { label: 'Bootstrap', value: 'bootstrap' },
206
+ { label: 'Bulma', value: 'bulma' },
207
+ { label: 'Foundation', value: 'foundation' },
208
+ { label: 'UIkit', value: 'uikit' },
209
+ { label: 'None', value: 'none' }
210
+ ];
211
+ let selectedIndex = 0;
212
+
213
+ log('\n>> Select a CSS framework (Use arrow keys and press Enter):\n');
214
+
215
+ const render = (firstTime = false) => {
216
+ if (!firstTime) {
217
+ process.stdout.write(`\x1B[${choices.length}A`);
218
+ }
219
+ let output = '';
220
+ choices.forEach((choice, index) => {
221
+ if (index === selectedIndex) {
222
+ output += ` \x1b[36m◉ ${choice.label}\x1b[0m\x1B[K\n`;
223
+ } else {
224
+ output += ` * ${choice.label}\x1B[K\n`;
225
+ }
226
+ });
227
+ process.stdout.write(output);
228
+ };
229
+
230
+ readline.emitKeypressEvents(process.stdin);
231
+ if (process.stdin.isTTY) {
232
+ process.stdin.setRawMode(true);
233
+ }
234
+ process.stdin.resume();
235
+
236
+ const onKeyPress = (str, key) => {
237
+ if (key.ctrl && key.name === 'c') {
238
+ process.exit();
239
+ } else if (key.name === 'up') {
240
+ selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : choices.length - 1;
241
+ render();
242
+ } else if (key.name === 'down') {
243
+ selectedIndex = selectedIndex < choices.length - 1 ? selectedIndex + 1 : 0;
244
+ render();
245
+ } else if (key.name === 'return' || key.name === 'enter') {
246
+ process.stdin.removeListener('keypress', onKeyPress);
247
+ if (process.stdin.isTTY) {
248
+ process.stdin.setRawMode(false);
249
+ }
250
+ process.stdin.pause();
251
+ resolve(choices[selectedIndex].value);
252
+ }
253
+ };
254
+
255
+ process.stdin.on('keypress', onKeyPress);
256
+ render(true);
257
+ });
109
258
  }
110
- deleteFileRecursive(targetDir, 'config.example.php');
111
-
112
- fs.writeFileSync(
113
- path.join(targetDir, 'package.json'),
114
- JSON.stringify(PROJECT_PACKAGE, null, 2)
115
- );
116
- log('+ package.json');
117
-
118
- fs.writeFileSync(
119
- path.join(targetDir, '.gitignore'),
120
- GITIGNORE_CONTENT
121
- );
122
- log('+ .gitignore');
123
-
124
- log(`\n>> Done! Now run:\n`);
125
- if (process.argv[2]) {
126
- log(`cd ${process.argv[2]}`);
259
+
260
+ async function init() {
261
+ if (!fs.existsSync(targetDir)) {
262
+ fs.mkdirSync(targetDir, { recursive: true });
263
+ }
264
+
265
+ log(`\n>> Creating berna-stencil project in ${targetDir}\n`);
266
+
267
+ for (const target of COPY_TARGETS) {
268
+ const src = path.join(templateDir, target);
269
+ const dest = path.join(targetDir, target);
270
+ if (fs.existsSync(src)) {
271
+ copyRecursive(src, dest);
272
+ log(`+ ${target}`);
273
+ }
274
+ }
275
+
276
+ const configDest = path.join(targetDir, 'src/backend/config.php');
277
+ const configExample = path.join(targetDir, 'src/backend/config.example.php');
278
+ if (!fs.existsSync(configDest) && fs.existsSync(configExample)) {
279
+ fs.copyFileSync(configExample, configDest);
280
+ log('+ src/backend/config.php');
281
+ }
282
+ deleteFileRecursive(targetDir, 'config.example.php');
283
+
284
+ fs.writeFileSync(
285
+ path.join(targetDir, 'package.json'),
286
+ JSON.stringify(PROJECT_PACKAGE, null, 2)
287
+ );
288
+ log('+ package.json');
289
+
290
+ fs.writeFileSync(
291
+ path.join(targetDir, '.gitignore'),
292
+ GITIGNORE_CONTENT
293
+ );
294
+ log('+ .gitignore');
295
+
296
+ const framework = await askFramework();
297
+ applyFramework(framework);
298
+
299
+ log(`\n>> Done! Now run:\n`);
300
+ if (process.argv[2]) {
301
+ log(`cd ${process.argv[2]}`);
302
+ }
303
+ log('npm install');
304
+ log('npm run serve\n');
127
305
  }
128
- log('npm install');
129
- log('npm run serve\n');
306
+
307
+ init();
package/docs/Backend.md CHANGED
@@ -9,7 +9,7 @@ src/backend/
9
9
  ├── api/
10
10
  │ ├── public/ # Endpoints accessible without an API key
11
11
  │ └── protected/ # Endpoints requiring X-Api-Key header
12
- ├── database/
12
+ ├── db/
13
13
  │ ├── Database.php
14
14
  │ ├── models/
15
15
  │ └── migrations/
@@ -26,11 +26,13 @@ Copy `config.example.php` to `config.php` and fill in your values:
26
26
  ### config.php <small>(`src/backend/`)</small>
27
27
  ```php
28
28
  return [
29
- 'APP_ENV' => 'development', // or 'production'
30
- 'API_KEY' => 'your-default-key',
29
+ // Default key for protected endpoints that don't have a specific key in ENDPOINT_KEYS
30
+ 'API_KEY' => 'default-key',
31
31
 
32
+ // If you want restrict access to protected endpoints to specific clients, you can define custom keys for each endpoint
33
+ // For subfolder endpoints, use the relative path ('subfolder/endpoint')
32
34
  'ENDPOINT_KEYS' => [
33
- 'subfolder/example-protected' => 'specific-key',
35
+ 'example-protected' => 'custom-key',
34
36
  ],
35
37
 
36
38
  'DB_HOST' => '127.0.0.1',
@@ -57,7 +59,7 @@ Every endpoint file has access to:
57
59
 
58
60
  Create a `.php` file anywhere inside `api/public/`
59
61
 
60
- ### api/public/posts.php
62
+ ### api/public/example.php
61
63
  ```php
62
64
  <?php
63
65
  declare(strict_types=1);
@@ -73,13 +75,11 @@ $id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
73
75
  Response::success(['id' => $id]);
74
76
  ```
75
77
 
76
- Reachable at `/api/posts` or `/api/posts/42`
77
-
78
78
  ## Creating a protected endpoint
79
79
 
80
80
  Create a `.php` file inside `api/protected/`. The API key check happens automatically before your file runs.
81
81
 
82
- ### api/protected/admin/stats.php
82
+ ### api/protected/example.php
83
83
  ```php
84
84
  <?php
85
85
  declare(strict_types=1);
@@ -97,114 +97,21 @@ To assign a dedicated key, add it to `config.php`:
97
97
 
98
98
  ```php
99
99
  'ENDPOINT_KEYS' => [
100
- 'admin/stats' => 'secret-stats-key',
100
+ 'endpoint' => 'custom-key',
101
101
  ],
102
102
  ```
103
103
 
104
104
  ## The Response helper
105
105
 
106
106
  ```php
107
- Response::success($data, $code); // default 200
107
+ Response::success($data, $code); // default 200
108
108
  Response::error($message, $code, $details); // default 400
109
- Response::noContent(); // 204
110
- ```
111
-
112
- ## Handling multiple methods
113
-
114
- ```php
115
- $id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
116
- $input = json_decode(file_get_contents('php://input'), true) ?? [];
117
-
118
- switch ($method) {
119
- case 'GET':
120
- Response::success(['id' => $id]);
121
- break;
122
-
123
- case 'POST':
124
- if (empty($input['title'])) Response::error('Missing title', 400);
125
- Response::success(['message' => 'Created'], 201);
126
- break;
127
-
128
- case 'DELETE':
129
- if (!$id) Response::error('ID required', 400);
130
- Response::success(['message' => 'Deleted']);
131
- break;
132
-
133
- default:
134
- Response::error('Method not allowed', 405);
135
- }
136
- ```
137
-
138
- ## Using the database
139
-
140
- ### database/models/Post.php
141
- ```php
142
- <?php
143
- declare(strict_types=1);
144
-
145
- require_once __DIR__ . '/../Database.php';
146
-
147
- class Post {
148
- private PDO $db;
149
-
150
- public function __construct() {
151
- $this->db = Database::getInstance();
152
- }
153
-
154
- public function getAll(): array {
155
- return $this->db->query("SELECT * FROM posts")->fetchAll();
156
- }
157
-
158
- public function getById(int $id): ?array {
159
- $stmt = $this->db->prepare("SELECT * FROM posts WHERE id = :id");
160
- $stmt->execute(['id' => $id]);
161
- return $stmt->fetch() ?: null;
162
- }
163
-
164
- public function create(string $title): int {
165
- $stmt = $this->db->prepare("INSERT INTO posts (title) VALUES (:title)");
166
- $stmt->execute(['title' => htmlspecialchars(strip_tags(trim($title)))]);
167
- return (int)$this->db->lastInsertId();
168
- }
169
- }
170
- ```
171
-
172
- Then use it inside an endpoint:
173
-
174
- ```php
175
- require_once __DIR__ . '/../../database/models/Post.php';
176
-
177
- $post = new Post();
178
- Response::success($post->getAll());
179
- ```
180
-
181
- Migrations live in `database/migrations/` as plain SQL files — run them manually against your database.
182
-
183
- ## Calling endpoints from the frontend
184
-
185
- ```js
186
- // Public
187
- const res = await fetch('/api/posts/42');
188
-
189
- // Protected
190
- const res = await fetch('/api/admin/stats', {
191
- headers: { 'X-Api-Key': 'secret-stats-key' }
192
- });
193
-
194
- // POST
195
- const res = await fetch('/api/posts', {
196
- method: 'POST',
197
- headers: { 'Content-Type': 'application/json', 'X-Api-Key': 'your-key' },
198
- body: JSON.stringify({ title: 'Hello world' })
199
- });
109
+ Response::noContent(); // 204
200
110
  ```
201
111
 
202
112
  ## Pre-built endpoints
203
113
 
204
- | Route | Auth | Methods | Description |
205
- |---|---|---|---|
206
- | `/api/example-public` | No | `GET` | Smoke test for public routing |
207
- | `/api/subfolder/example-protected` | Yes | `GET` | Smoke test for protected routing |
208
- | `/api/auth/register` | No | `POST` | Register a new user |
209
- | `/api/auth/login` | No | `POST` | Login and retrieve user data |
210
- | `/api/auth-system` | Yes | `GET POST PUT PATCH DELETE` | Full CRUD on users |
114
+ | Route | Method | Description |
115
+ |---|---|---|
116
+ | `/api/example-public` | `GET` | Example endpoint that doesn't require any key |
117
+ | `/api/example-protected` | `GET` | Example endpoint that requires X-API-KEY |
@@ -10,11 +10,12 @@ Import only what the page needs.
10
10
 
11
11
  ### examplePage.js <small>(`src/frontend/js/pages/`)</small>
12
12
  ```js
13
- import { initLangSwitcher } from '../modules/langSwitcher.js';
14
13
  import { showNotification } from '../modules/notification.js';
15
14
 
15
+ import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
16
+
16
17
  document.addEventListener("DOMContentLoaded", () => {
17
- initLangSwitcher();
18
+ initNormalizePhoneNumber();
18
19
  });
19
20
 
20
21
  showNotification("Page loaded", "success", 3000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-berna-stencil",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
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",
@@ -58,4 +58,4 @@
58
58
  "assistant": "node _tools/assistant.js",
59
59
  "postinstall": "cd src/backend/_core && composer install --quiet"
60
60
  }
61
- }
61
+ }
@@ -2,12 +2,13 @@
2
2
  declare(strict_types=1);
3
3
 
4
4
  return [
5
- 'API_KEY' => 'DEFAULT_KEY', // Default key for protected endpoints that don't have a specific key in ENDPOINT_KEYS
5
+ // Default key for protected endpoints that don't have a specific key in ENDPOINT_KEYS
6
+ 'API_KEY' => 'DEFAULT_KEY',
6
7
 
7
8
  // If you want restrict access to protected endpoints to specific clients, you can define custom keys for each endpoint
8
9
  // For subfolder endpoints, use the relative path ('subfolder/endpoint')
9
10
  'ENDPOINT_KEYS' => [
10
- 'subfolder/example-protected' => 'example-key',
11
+ 'subfolder/example-protected' => 'custom-key',
11
12
  ],
12
13
 
13
14
  // Database configuration
@@ -2,7 +2,8 @@
2
2
  declare(strict_types=1);
3
3
 
4
4
  return [
5
- 'API_KEY' => 'DEFAULT_KEY', // Default key for protected endpoints that don't have a specific key in ENDPOINT_KEYS
5
+ // Default key for protected endpoints that don't have a specific key in ENDPOINT_KEYS
6
+ 'API_KEY' => 'DEFAULT_KEY',
6
7
 
7
8
  // If you want restrict access to protected endpoints to specific clients, you can define custom keys for each endpoint
8
9
  // For subfolder endpoints, use the relative path ('subfolder/endpoint')
@@ -7,7 +7,7 @@ layout: base.njk
7
7
  <!-- !IMPORTANT -->
8
8
  <!-- This is the only page that you need to modify statically -->
9
9
 
10
- <div class="fade-in d-flex flex-column justify-content-center align-items-center" style="min-height: 60vh">
10
+ <div class="fade-in center">
11
11
  <h1>Oops! Page not found</h1>
12
12
  <a href="/">Return to homepage</a>
13
13
  </div>