create-berna-stencil 2.6.3 → 2.6.5
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/index.js +3 -0
- package/package.json +4 -27
- package/.eleventy.js +0 -94
- package/.eleventyignore +0 -4
- package/LICENSE +0 -170
- package/NOTICE +0 -5
- package/README.md +0 -53
- package/_tools/assistant.js +0 -150
- package/_tools/buildJs.js +0 -28
- package/_tools/cleanOutput.js +0 -20
- package/_tools/modules/constants.js +0 -23
- package/_tools/modules/pageComponents.js +0 -77
- package/_tools/modules/updateData.js +0 -84
- 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 -20
- package/_tools/res/templates/template.js +0 -5
- package/_tools/res/templates/template.njk +0 -9
- package/_tools/res/templates/template.scss +0 -23
- package/_tools/res/templates/template.ts +0 -5
- package/bin/create.js +0 -429
- package/docs/Assistant CLI.md +0 -61
- package/docs/Backend.md +0 -148
- package/docs/Components.md +0 -96
- package/docs/Creating pages.md +0 -65
- package/docs/Deploy.md +0 -55
- package/docs/Head and SEO.md +0 -94
- package/docs/Javascript.md +0 -53
- package/docs/Styling with SCSS.md +0 -140
- package/nginx.conf +0 -69
- package/src/backend/.htaccess +0 -7
- package/src/backend/_core/composer.json +0 -5
- package/src/backend/_core/composer.lock +0 -492
- package/src/backend/_core/index.php +0 -148
- package/src/backend/_core/init.php +0 -34
- package/src/backend/_core/modules/RateLimiter.php +0 -31
- package/src/backend/_core/modules/Response.php +0 -49
- package/src/backend/api/protected/example-protected.php +0 -17
- package/src/backend/api/public/example-public.php +0 -17
- package/src/backend/database/Database.php +0 -24
- package/src/backend/database/migrations/create_example_db.sql +0 -1
- package/src/backend/web.config +0 -17
- package/src/frontend/.htaccess +0 -16
- package/src/frontend/404.njk +0 -17
- package/src/frontend/assets/brand/favicon.svg +0 -37
- package/src/frontend/assets/brand/logo.svg +0 -37
- package/src/frontend/components/global/footer.njk +0 -27
- package/src/frontend/components/global/header.njk +0 -11
- package/src/frontend/components/welcome.njk +0 -251
- package/src/frontend/data/site.json +0 -44
- package/src/frontend/index.njk +0 -9
- package/src/frontend/js/modules/exampleModule.js +0 -3
- package/src/frontend/js/pages/404.js +0 -7
- package/src/frontend/js/pages/homepage.js +0 -7
- package/src/frontend/layouts/base.njk +0 -137
- package/src/frontend/layouts/pageComponents.njk +0 -14
- package/src/frontend/llms.njk +0 -18
- package/src/frontend/robots.njk +0 -8
- package/src/frontend/scss/modules/_animations.scss +0 -25
- package/src/frontend/scss/modules/_buttons.scss +0 -24
- package/src/frontend/scss/modules/_footer.scss +0 -32
- package/src/frontend/scss/modules/_global.scss +0 -46
- package/src/frontend/scss/modules/_header.scss +0 -33
- package/src/frontend/scss/modules/_mobile.scss +0 -30
- package/src/frontend/scss/modules/_notification.scss +0 -56
- package/src/frontend/scss/modules/_root.scss +0 -35
- package/src/frontend/scss/modules/_typography.scss +0 -15
- package/src/frontend/scss/modules/frameworks/_bootstrap.scss +0 -110
- package/src/frontend/scss/modules/frameworks/_bulma.scss +0 -109
- package/src/frontend/scss/modules/frameworks/_foundation.scss +0 -139
- package/src/frontend/scss/modules/frameworks/_uikit.scss +0 -110
- package/src/frontend/scss/pages/404.scss +0 -28
- package/src/frontend/scss/pages/homepage.scss +0 -23
- package/src/frontend/sitemap.njk +0 -17
- package/src/frontend/ts/modules/exampleModule.ts +0 -3
- package/src/frontend/ts/pages/404.ts +0 -7
- package/src/frontend/ts/pages/homepage.ts +0 -7
- package/src/frontend/web.config +0 -27
- package/tsconfig.json +0 -25
package/bin/create.js
DELETED
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const readline = require('readline');
|
|
6
|
-
const { writeSync } = require('fs');
|
|
7
|
-
const { spawnSync } = require('child_process');
|
|
8
|
-
|
|
9
|
-
const color = {
|
|
10
|
-
reset: '\x1b[0m',
|
|
11
|
-
bold: '\x1b[1m',
|
|
12
|
-
dim: '\x1b[2m',
|
|
13
|
-
red: '\x1b[31m',
|
|
14
|
-
green: '\x1b[32m',
|
|
15
|
-
yellow: '\x1b[33m',
|
|
16
|
-
blue: '\x1b[34m',
|
|
17
|
-
magenta: '\x1b[35m',
|
|
18
|
-
cyan: '\x1b[36m',
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
// ── PATHS ────────────────────────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
const targetDir = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
|
|
24
|
-
const templateDir = path.join(__dirname, '..');
|
|
25
|
-
|
|
26
|
-
// ── ENUMS ────────────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
const LANGUAGE = Object.freeze({
|
|
29
|
-
JAVASCRIPT: 'javascript',
|
|
30
|
-
TYPESCRIPT: 'typescript',
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const FRAMEWORK = Object.freeze({
|
|
34
|
-
BOOTSTRAP: 'bootstrap',
|
|
35
|
-
BULMA: 'bulma',
|
|
36
|
-
FOUNDATION: 'foundation',
|
|
37
|
-
UIKIT: 'uikit',
|
|
38
|
-
NONE: 'none',
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// ── CHOICES ──────────────────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
const LANGUAGE_CHOICES = [
|
|
44
|
-
{ label: 'JavaScript (default)', value: LANGUAGE.JAVASCRIPT },
|
|
45
|
-
{ label: 'TypeScript', value: LANGUAGE.TYPESCRIPT },
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
const FRAMEWORK_CHOICES = [
|
|
49
|
-
{ label: 'Bootstrap (default)', value: FRAMEWORK.BOOTSTRAP },
|
|
50
|
-
{ label: 'Bulma', value: FRAMEWORK.BULMA },
|
|
51
|
-
{ label: 'Foundation', value: FRAMEWORK.FOUNDATION },
|
|
52
|
-
{ label: 'UIkit', value: FRAMEWORK.UIKIT },
|
|
53
|
-
{ label: 'None', value: FRAMEWORK.NONE },
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
// ── COPY CONFIG ───────────────────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
const MANDATORY_COPY = [
|
|
59
|
-
'docs',
|
|
60
|
-
'_tools',
|
|
61
|
-
'.eleventy.js',
|
|
62
|
-
'.eleventyignore',
|
|
63
|
-
'nginx.conf',
|
|
64
|
-
'src/backend',
|
|
65
|
-
'src/frontend',
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
const FRONTEND_EXCLUDE = {
|
|
69
|
-
[LANGUAGE.JAVASCRIPT]: ['ts'],
|
|
70
|
-
[LANGUAGE.TYPESCRIPT]: ['js'],
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const CREATE_DIRS = [
|
|
74
|
-
'src/frontend/_routes',
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
// ── FRAMEWORK CONFIG ──────────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
const ALL_FRAMEWORKS = Object.values(FRAMEWORK).filter(f => f !== FRAMEWORK.NONE);
|
|
80
|
-
|
|
81
|
-
const FRAMEWORKS = {
|
|
82
|
-
[FRAMEWORK.BOOTSTRAP]: {
|
|
83
|
-
scss: 'bootstrap',
|
|
84
|
-
njk: ['<script src="/js/bootstrap.bundle.min.js" defer></script>'],
|
|
85
|
-
eleventy: [
|
|
86
|
-
'"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js": "js/bootstrap.bundle.min.js",',
|
|
87
|
-
'"node_modules/bootstrap-icons/font/fonts": "css/fonts",',
|
|
88
|
-
],
|
|
89
|
-
},
|
|
90
|
-
[FRAMEWORK.BULMA]: {
|
|
91
|
-
scss: 'bulma',
|
|
92
|
-
njk: [],
|
|
93
|
-
eleventy: [],
|
|
94
|
-
},
|
|
95
|
-
[FRAMEWORK.FOUNDATION]: {
|
|
96
|
-
scss: 'foundation',
|
|
97
|
-
njk: ['<script src="/js/foundation.min.js" defer></script>'],
|
|
98
|
-
eleventy: ['"node_modules/foundation-sites/dist/js/foundation.min.js": "js/foundation.min.js",'],
|
|
99
|
-
},
|
|
100
|
-
[FRAMEWORK.UIKIT]: {
|
|
101
|
-
scss: 'uikit',
|
|
102
|
-
njk: [
|
|
103
|
-
'<script src="/js/uikit.min.js" defer></script>',
|
|
104
|
-
'<script src="/js/uikit-icons.min.js" defer></script>',
|
|
105
|
-
],
|
|
106
|
-
eleventy: [
|
|
107
|
-
'"node_modules/uikit/dist/js/uikit.min.js": "js/uikit.min.js",',
|
|
108
|
-
'"node_modules/uikit/dist/js/uikit-icons.min.js": "js/uikit-icons.min.js",',
|
|
109
|
-
],
|
|
110
|
-
},
|
|
111
|
-
[FRAMEWORK.NONE]: {
|
|
112
|
-
scss: null,
|
|
113
|
-
njk: [],
|
|
114
|
-
eleventy: [],
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// ── LANGUAGE CONFIG ───────────────────────────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
const LANGUAGE_ELEVENTY = Object.freeze({
|
|
121
|
-
jsEntry: 'const entryPoints = glob.sync("src/frontend/js/pages/*.js");',
|
|
122
|
-
tsEntry: 'const entryPoints = glob.sync("src/frontend/ts/pages/*.ts");',
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// ── GENERATED FILE CONTENTS ───────────────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
const GITIGNORE_CONTENT = `
|
|
128
|
-
node_modules/
|
|
129
|
-
src/backend/_core/vendor/
|
|
130
|
-
out/
|
|
131
|
-
src/backend/config.php
|
|
132
|
-
`;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const PROJECT_PACKAGE = {
|
|
136
|
-
name: path.basename(targetDir),
|
|
137
|
-
version: '2.6.0',
|
|
138
|
-
private: true,
|
|
139
|
-
outputDir: 'out',
|
|
140
|
-
"scripts": {
|
|
141
|
-
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet --load-path=node_modules",
|
|
142
|
-
"build:js": "node _tools/buildJs.js",
|
|
143
|
-
"build:11ty": "eleventy",
|
|
144
|
-
"build": "npm run clean && npm run build:css && npm run build:js && npm run build:11ty",
|
|
145
|
-
"serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet --load-path=node_modules",
|
|
146
|
-
"serve:js": "node _tools/buildJs.js --watch",
|
|
147
|
-
"serve:11ty": "eleventy --serve --quiet",
|
|
148
|
-
"clean": "node _tools/cleanOutput.js",
|
|
149
|
-
"serve": "npm run clean && concurrently \"npm run serve:11ty\" \"npm run serve:css\" \"npm run serve:js\"",
|
|
150
|
-
"assistant": "node _tools/assistant.js"
|
|
151
|
-
},
|
|
152
|
-
dependencies: {
|
|
153
|
-
'@11ty/eleventy': '^3.1.2',
|
|
154
|
-
'@11ty/eleventy-img': '^6.0.4',
|
|
155
|
-
'bootstrap': '^5.3.8',
|
|
156
|
-
'bootstrap-icons': '^1.13.1',
|
|
157
|
-
'bulma': '^1.0.4',
|
|
158
|
-
'foundation-sites': '^6.9.0',
|
|
159
|
-
'github-markdown-css': '^5.9.0',
|
|
160
|
-
'glob': '^13.0.6',
|
|
161
|
-
'uikit': '^3.25.13',
|
|
162
|
-
},
|
|
163
|
-
devDependencies: {
|
|
164
|
-
'concurrently': '^9.2.1',
|
|
165
|
-
'esbuild': '^0.27.3',
|
|
166
|
-
'sass': '^1.77.0',
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
// ── HELPERS ───────────────────────────────────────────────────────────────────
|
|
171
|
-
|
|
172
|
-
function log(msg) {
|
|
173
|
-
writeSync(1, msg + '\n');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function logAdd(name) {
|
|
177
|
-
log(`${color.green}+${color.reset} ${name}`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function escapeRegex(str) {
|
|
181
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function copyRecursive(src, dest, exclude = []) {
|
|
185
|
-
const stat = fs.statSync(src);
|
|
186
|
-
if (stat.isDirectory()) {
|
|
187
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
188
|
-
for (const child of fs.readdirSync(src)) {
|
|
189
|
-
if (child === '.git') continue;
|
|
190
|
-
if (exclude.includes(child)) continue;
|
|
191
|
-
copyRecursive(path.join(src, child), path.join(dest, child), exclude);
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
195
|
-
fs.copyFileSync(src, dest);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function deleteFileRecursive(dir, filename) {
|
|
200
|
-
for (const child of fs.readdirSync(dir)) {
|
|
201
|
-
const fullPath = path.join(dir, child);
|
|
202
|
-
if (fs.statSync(fullPath).isDirectory()) {
|
|
203
|
-
deleteFileRecursive(fullPath, filename);
|
|
204
|
-
} else if (child === filename) {
|
|
205
|
-
fs.unlinkSync(fullPath);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function slashComment(content, line) {
|
|
211
|
-
content = content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
|
|
212
|
-
return content.replace(new RegExp(`^([ \\t]*)(${escapeRegex(line)})$`, 'gm'), '$1// $2');
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function slashUncomment(content, line) {
|
|
216
|
-
return content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function njkComment(content, line) {
|
|
220
|
-
content = content.split(`{# ${line} #}`).join(line);
|
|
221
|
-
return content.split(line).join(`{# ${line} #}`);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function njkUncomment(content, line) {
|
|
225
|
-
return content.split(`{# ${line} #}`).join(line);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function installDependencies() {
|
|
229
|
-
const backendCore = path.join(targetDir, 'src', 'backend', '_core');
|
|
230
|
-
|
|
231
|
-
log(`${color.blue}\n>> Installing Node modules...${color.reset}`);
|
|
232
|
-
const npm = spawnSync('npm', ['install'], {
|
|
233
|
-
cwd: targetDir,
|
|
234
|
-
stdio: 'inherit',
|
|
235
|
-
shell: process.platform === 'win32',
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
if (npm.status !== 0) {
|
|
239
|
-
log('\n(!) npm install failed. Finish manually:');
|
|
240
|
-
if (process.argv[2]) log(` cd ${process.argv[2]}`);
|
|
241
|
-
log(' npm install\n');
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (!fs.existsSync(path.join(backendCore, 'composer.json'))) {
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const probe = spawnSync('composer', ['--version'], {
|
|
250
|
-
stdio: 'ignore',
|
|
251
|
-
shell: process.platform === 'win32',
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
if (probe.status !== 0) {
|
|
255
|
-
log('\n(!) Composer not found — skipping backend dependencies.');
|
|
256
|
-
log(' Install Composer, then run: cd src/backend/_core && composer install\n');
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
log(`\n${color.blue}>> Installing Composer modules...${color.reset}\n`);
|
|
261
|
-
spawnSync('composer', ['install'], {
|
|
262
|
-
cwd: backendCore,
|
|
263
|
-
stdio: 'inherit',
|
|
264
|
-
shell: process.platform === 'win32',
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// ── APPLY ─────────────────────────────────────────────────────────────────────
|
|
271
|
-
|
|
272
|
-
function applyFramework(framework) {
|
|
273
|
-
const config = FRAMEWORKS[framework];
|
|
274
|
-
|
|
275
|
-
const globalScssPath = path.join(targetDir, 'src/frontend/scss/modules/_global.scss');
|
|
276
|
-
if (fs.existsSync(globalScssPath)) {
|
|
277
|
-
let content = fs.readFileSync(globalScssPath, 'utf8');
|
|
278
|
-
ALL_FRAMEWORKS.forEach(fw => {
|
|
279
|
-
content = slashComment(content, `@import "../modules/frameworks/${fw}";`);
|
|
280
|
-
});
|
|
281
|
-
if (config.scss) {
|
|
282
|
-
content = slashUncomment(content, `@import "../modules/frameworks/${config.scss}";`);
|
|
283
|
-
}
|
|
284
|
-
fs.writeFileSync(globalScssPath, content);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const baseNjkPath = path.join(targetDir, 'src/frontend/layouts/base.njk');
|
|
288
|
-
if (fs.existsSync(baseNjkPath)) {
|
|
289
|
-
let content = fs.readFileSync(baseNjkPath, 'utf8');
|
|
290
|
-
ALL_FRAMEWORKS.forEach(fw => {
|
|
291
|
-
FRAMEWORKS[fw].njk.forEach(line => { content = njkComment(content, line); });
|
|
292
|
-
});
|
|
293
|
-
config.njk.forEach(line => { content = njkUncomment(content, line); });
|
|
294
|
-
fs.writeFileSync(baseNjkPath, content);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const eleventyPath = path.join(targetDir, '.eleventy.js');
|
|
298
|
-
if (fs.existsSync(eleventyPath)) {
|
|
299
|
-
let content = fs.readFileSync(eleventyPath, 'utf8');
|
|
300
|
-
ALL_FRAMEWORKS.forEach(fw => {
|
|
301
|
-
FRAMEWORKS[fw].eleventy.forEach(line => { content = slashComment(content, line); });
|
|
302
|
-
});
|
|
303
|
-
config.eleventy.forEach(line => { content = slashUncomment(content, line); });
|
|
304
|
-
fs.writeFileSync(eleventyPath, content);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function applyLanguage(language) {
|
|
309
|
-
const eleventyPath = path.join(targetDir, '.eleventy.js');
|
|
310
|
-
if (!fs.existsSync(eleventyPath)) return;
|
|
311
|
-
|
|
312
|
-
let content = fs.readFileSync(eleventyPath, 'utf8');
|
|
313
|
-
|
|
314
|
-
if (language === LANGUAGE.TYPESCRIPT) {
|
|
315
|
-
content = slashComment(content, LANGUAGE_ELEVENTY.jsEntry);
|
|
316
|
-
content = slashUncomment(content, LANGUAGE_ELEVENTY.tsEntry);
|
|
317
|
-
} else {
|
|
318
|
-
content = slashUncomment(content, LANGUAGE_ELEVENTY.jsEntry);
|
|
319
|
-
content = slashComment(content, LANGUAGE_ELEVENTY.tsEntry);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
fs.writeFileSync(eleventyPath, content);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// ── UI ────────────────────────────────────────────────────────────────────────
|
|
326
|
-
|
|
327
|
-
function askChoice(question, choices) {
|
|
328
|
-
return new Promise((resolve) => {
|
|
329
|
-
let selectedIndex = 0;
|
|
330
|
-
|
|
331
|
-
log(`\n>> ${question} (Use arrow keys and press Enter):\n`);
|
|
332
|
-
|
|
333
|
-
const render = (firstTime = false) => {
|
|
334
|
-
if (!firstTime) process.stdout.write(`\x1B[${choices.length}A`);
|
|
335
|
-
const output = choices.map((choice, index) =>
|
|
336
|
-
index === selectedIndex
|
|
337
|
-
? ` \x1b[36m◉ ${choice.label}\x1b[0m\x1B[K\n`
|
|
338
|
-
: ` * ${choice.label}\x1B[K\n`
|
|
339
|
-
).join('');
|
|
340
|
-
process.stdout.write(output);
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
readline.emitKeypressEvents(process.stdin);
|
|
344
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
345
|
-
process.stdin.resume();
|
|
346
|
-
|
|
347
|
-
const onKeyPress = (str, key) => {
|
|
348
|
-
if (key.ctrl && key.name === 'c') {
|
|
349
|
-
process.exit();
|
|
350
|
-
} else if (key.name === 'up') {
|
|
351
|
-
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : choices.length - 1;
|
|
352
|
-
render();
|
|
353
|
-
} else if (key.name === 'down') {
|
|
354
|
-
selectedIndex = selectedIndex < choices.length - 1 ? selectedIndex + 1 : 0;
|
|
355
|
-
render();
|
|
356
|
-
} else if (key.name === 'return' || key.name === 'enter') {
|
|
357
|
-
process.stdin.removeListener('keypress', onKeyPress);
|
|
358
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
359
|
-
process.stdin.pause();
|
|
360
|
-
resolve(choices[selectedIndex].value);
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
process.stdin.on('keypress', onKeyPress);
|
|
365
|
-
render(true);
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// ── INIT ──────────────────────────────────────────────────────────────────────
|
|
370
|
-
|
|
371
|
-
async function init() {
|
|
372
|
-
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
373
|
-
|
|
374
|
-
log(`\n>> ${color.magenta}Creating berna-stencil project in ${targetDir}\n${color.reset}`);
|
|
375
|
-
|
|
376
|
-
const language = await askChoice('Select a language', LANGUAGE_CHOICES);
|
|
377
|
-
const framework = await askChoice('Select a CSS framework', FRAMEWORK_CHOICES);
|
|
378
|
-
|
|
379
|
-
log('');
|
|
380
|
-
|
|
381
|
-
for (const target of MANDATORY_COPY) {
|
|
382
|
-
const src = path.join(templateDir, target);
|
|
383
|
-
const dest = path.join(targetDir, target);
|
|
384
|
-
if (!fs.existsSync(src)) continue;
|
|
385
|
-
const exclude = target === 'src/frontend' ? FRONTEND_EXCLUDE[language] : [];
|
|
386
|
-
copyRecursive(src, dest, exclude);
|
|
387
|
-
logAdd(target);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const configDest = path.join(targetDir, 'src/backend/config.php');
|
|
391
|
-
const configExample = path.join(targetDir, 'src/backend/config.example.php');
|
|
392
|
-
if (!fs.existsSync(configDest) && fs.existsSync(configExample)) {
|
|
393
|
-
fs.copyFileSync(configExample, configDest);
|
|
394
|
-
logAdd('src/backend/config.php');
|
|
395
|
-
}
|
|
396
|
-
deleteFileRecursive(targetDir, 'config.example.php');
|
|
397
|
-
|
|
398
|
-
const pkg = { ...PROJECT_PACKAGE };
|
|
399
|
-
|
|
400
|
-
if (language === LANGUAGE.TYPESCRIPT) {
|
|
401
|
-
const tsSrc = path.join(templateDir, 'tsconfig.json');
|
|
402
|
-
const tsDest = path.join(targetDir, 'tsconfig.json');
|
|
403
|
-
fs.copyFileSync(tsSrc, tsDest);
|
|
404
|
-
logAdd('tsconfig.json');
|
|
405
|
-
pkg.devDependencies = { ...pkg.devDependencies, typescript: 'latest' };
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
409
|
-
logAdd('package.json');
|
|
410
|
-
|
|
411
|
-
fs.writeFileSync(path.join(targetDir, '.gitignore'), GITIGNORE_CONTENT);
|
|
412
|
-
logAdd('.gitignore');
|
|
413
|
-
|
|
414
|
-
for (const dir of CREATE_DIRS) {
|
|
415
|
-
fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
applyFramework(framework);
|
|
419
|
-
applyLanguage(language);
|
|
420
|
-
|
|
421
|
-
installDependencies();
|
|
422
|
-
|
|
423
|
-
log(`\n${color.green}>> Done!${color.reset}`);
|
|
424
|
-
log(`${color.yellow}\nNow run:\n${color.reset}`);
|
|
425
|
-
if (process.argv[2]) log(` ${color.yellow}cd ${process.argv[2]}${color.reset}`);
|
|
426
|
-
log(` ${color.yellow}npm run serve${color.reset}\n`);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
init();
|
package/docs/Assistant CLI.md
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# Assistant CLI
|
|
2
|
-
|
|
3
|
-
> Examples use JavaScript, but everything applies equally to TypeScript. The only difference is the file extension (`.ts` instead of `.js`), that imports do **not** include the extension, and that paths use `src/frontend/ts/` instead of `src/frontend/js/`.
|
|
4
|
-
|
|
5
|
-
An interactive CLI to manage pages without touching files manually.
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
npm run assistant
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Menu
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
1. Create page
|
|
15
|
-
2. Remove page
|
|
16
|
-
3. Rename page
|
|
17
|
-
4. Configure output path
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Use `CTRL/CMD + C` to exit.
|
|
21
|
-
|
|
22
|
-
## Create page
|
|
23
|
-
|
|
24
|
-
Enter a page name in any format — the CLI converts it to kebab-case automatically.
|
|
25
|
-
|
|
26
|
-
For a page named `my-page`, the following files are created:
|
|
27
|
-
|
|
28
|
-
| File | Purpose |
|
|
29
|
-
|---|---|
|
|
30
|
-
| `src/frontend/scss/pages/myPage.scss` | SCSS entry point |
|
|
31
|
-
| `src/frontend/js/pages/myPage.js` | JS entry point |
|
|
32
|
-
| `src/frontend/_routes/my-page.njk` | Nunjucks template |
|
|
33
|
-
|
|
34
|
-
It also adds an `elif` block in `pageComponents.njk` and a stub entry in `site.json`:
|
|
35
|
-
|
|
36
|
-
```json
|
|
37
|
-
"myPage": {
|
|
38
|
-
"seo": {
|
|
39
|
-
"title": "My Page",
|
|
40
|
-
"description": "description"
|
|
41
|
-
},
|
|
42
|
-
"cdn": {
|
|
43
|
-
"css": [],
|
|
44
|
-
"js": []
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Remove page
|
|
50
|
-
|
|
51
|
-
Deletes all source files for the page and cleans up the output directory, `pageComponents.njk`, and `site.json`.
|
|
52
|
-
|
|
53
|
-
## Rename page
|
|
54
|
-
|
|
55
|
-
Renames all three source files, updates the `elif` block in `pageComponents.njk`, and renames the record in `site.json` while preserving all existing fields.
|
|
56
|
-
|
|
57
|
-
## Configure output path
|
|
58
|
-
|
|
59
|
-
Updates the output directory across `.eleventy.js` and all relevant `package.json` scripts in one shot. The old output folder is deleted automatically.
|
|
60
|
-
|
|
61
|
-
> ⚠️ `homepage` and `404` are protected — they cannot be created, removed, or renamed via the CLI.
|
package/docs/Backend.md
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
# Backend
|
|
2
|
-
|
|
3
|
-
> **PHP requirement** — the backend runs only on servers with PHP support:
|
|
4
|
-
> - **Apache** (shared hosting, VPS) ✅
|
|
5
|
-
> - **Nginx** + PHP-FPM (VPS) ✅
|
|
6
|
-
> - **IIS** + FastCGI (Windows Server) ✅
|
|
7
|
-
> - **Static hosting** (Netlify, Vercel, GitHub Pages, Cloudflare Pages) ❌
|
|
8
|
-
|
|
9
|
-
The backend is a PHP REST API located in `src/backend/`, copied to the output directory automatically at build time.
|
|
10
|
-
|
|
11
|
-
## Structure
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
src/backend/
|
|
15
|
-
├── api/
|
|
16
|
-
│ ├── public/ # Endpoints accessible without an API key
|
|
17
|
-
│ └── protected/ # Endpoints requiring X-Api-Key header
|
|
18
|
-
├── db/
|
|
19
|
-
│ ├── Database.php
|
|
20
|
-
│ ├── models/
|
|
21
|
-
│ └── migrations/
|
|
22
|
-
├── config.php # Your local config — never commit this
|
|
23
|
-
└── config.example.php
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Configuration
|
|
27
|
-
|
|
28
|
-
`config.php` works like a `.env` file — it holds secrets and environment settings that stay local and out of version control.
|
|
29
|
-
|
|
30
|
-
Copy `config.example.php` to `config.php` and fill in your values:
|
|
31
|
-
|
|
32
|
-
### config.php <small>(`src/backend/`)</small>
|
|
33
|
-
```php
|
|
34
|
-
return [
|
|
35
|
-
// Default key for protected endpoints that don't have a specific key in CUSTOM_ENDPOINT_KEYS
|
|
36
|
-
'GENERAL_API_KEY' => 'default-key',
|
|
37
|
-
|
|
38
|
-
// Per-endpoint keys. For subfolder endpoints, use the relative path ('subfolder/endpoint')
|
|
39
|
-
'CUSTOM_ENDPOINT_KEYS' => [
|
|
40
|
-
// 'subfolder/endpoint' => 'example-key',
|
|
41
|
-
],
|
|
42
|
-
|
|
43
|
-
// Default CORS origins applied to every endpoint
|
|
44
|
-
'GENERAL_ALLOWED_ORIGINS' => [
|
|
45
|
-
'https://example.com',
|
|
46
|
-
],
|
|
47
|
-
|
|
48
|
-
// Per-endpoint CORS origins. Same path format as CUSTOM_ENDPOINT_KEYS.
|
|
49
|
-
// When an endpoint is listed here, these origins replace GENERAL_ALLOWED_ORIGINS for it.
|
|
50
|
-
'CUSTOM_ENDPOINT_ORIGINS' => [
|
|
51
|
-
// 'subfolder/endpoint' => ['https://app.example.com'],
|
|
52
|
-
],
|
|
53
|
-
|
|
54
|
-
'DB_HOST' => '127.0.0.1',
|
|
55
|
-
'DB_NAME' => 'example_db',
|
|
56
|
-
'DB_USER' => 'root',
|
|
57
|
-
'DB_PASS' => '',
|
|
58
|
-
];
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### API keys
|
|
62
|
-
|
|
63
|
-
`GENERAL_API_KEY` is the fallback key for all protected endpoints. Use `CUSTOM_ENDPOINT_KEYS` to assign a different key to a specific endpoint — for subfolder endpoints, use the relative path as the key.
|
|
64
|
-
|
|
65
|
-
> ⚠️ The API key travels in the `X-Api-Key` header on every request. Use it only for server-to-server calls over HTTPS. Never embed it in frontend code, where it would be publicly visible.
|
|
66
|
-
|
|
67
|
-
### CORS (allowed origins)
|
|
68
|
-
|
|
69
|
-
Cross-origin access mirrors the API-key model:
|
|
70
|
-
|
|
71
|
-
- `GENERAL_ALLOWED_ORIGINS` — the default list of origins allowed to call any endpoint from a browser.
|
|
72
|
-
- `CUSTOM_ENDPOINT_ORIGINS` — overrides the default for a specific endpoint, keyed by the same relative path used in `CUSTOM_ENDPOINT_KEYS`.
|
|
73
|
-
|
|
74
|
-
Origins must be exact (scheme + host, no trailing slash), e.g. `https://example.com`. When a request's `Origin` is in the resolved list, it is reflected back in `Access-Control-Allow-Origin` (with `Vary: Origin`). An empty list sends no CORS header — the most restrictive setting; same-origin requests still work. Use `'*'` as the only element to allow any origin (not recommended for protected endpoints).
|
|
75
|
-
|
|
76
|
-
Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if present, otherwise `GENERAL_ALLOWED_ORIGINS`.
|
|
77
|
-
|
|
78
|
-
## How routing works
|
|
79
|
-
|
|
80
|
-
The file path inside `api/` maps directly to the URL. Extra URL segments become route parameters available as `$requestParams[]`.
|
|
81
|
-
|
|
82
|
-
Every endpoint file has access to:
|
|
83
|
-
|
|
84
|
-
| Variable | Description |
|
|
85
|
-
|---|---|
|
|
86
|
-
| `$method` | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) |
|
|
87
|
-
| `$requestParams` | Extra URL segments (e.g. `/api/posts/42` → `['42']`) |
|
|
88
|
-
|
|
89
|
-
## Creating a public endpoint
|
|
90
|
-
|
|
91
|
-
Create a `.php` file anywhere inside `api/public/`
|
|
92
|
-
|
|
93
|
-
### api/public/example.php
|
|
94
|
-
```php
|
|
95
|
-
<?php
|
|
96
|
-
declare(strict_types=1);
|
|
97
|
-
|
|
98
|
-
require_once CORE_PATH . '/modules/Response.php';
|
|
99
|
-
|
|
100
|
-
if ($method !== 'GET') {
|
|
101
|
-
Response::error('Method not allowed', 405);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
$id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
|
|
105
|
-
|
|
106
|
-
Response::success(['id' => $id]);
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Creating a protected endpoint
|
|
110
|
-
|
|
111
|
-
Create a `.php` file inside `api/protected/`. The API key check happens automatically before your file runs.
|
|
112
|
-
|
|
113
|
-
### api/protected/example.php
|
|
114
|
-
```php
|
|
115
|
-
<?php
|
|
116
|
-
declare(strict_types=1);
|
|
117
|
-
|
|
118
|
-
require_once CORE_PATH . '/modules/Response.php';
|
|
119
|
-
|
|
120
|
-
if ($method !== 'GET') {
|
|
121
|
-
Response::error('Method not allowed', 405);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
Response::success(['visits' => 1024]);
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
To assign a dedicated key, add it to `config.php`:
|
|
128
|
-
|
|
129
|
-
```php
|
|
130
|
-
'CUSTOM_ENDPOINT_KEYS' => [
|
|
131
|
-
'endpoint' => 'custom-key',
|
|
132
|
-
],
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## The Response helper
|
|
136
|
-
|
|
137
|
-
```php
|
|
138
|
-
Response::success($data, $code); // default 200
|
|
139
|
-
Response::error($message, $code, $details); // default 400
|
|
140
|
-
Response::noContent(); // 204
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## Pre-built endpoints
|
|
144
|
-
|
|
145
|
-
| Route | Method | Description |
|
|
146
|
-
|---|---|---|
|
|
147
|
-
| `/api/example-public` | `GET` | Example endpoint that doesn't require any key |
|
|
148
|
-
| `/api/example-protected` | `GET` | Example endpoint that requires X-API-KEY |
|