create-berna-stencil 2.1.0 → 2.2.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/.eleventy.js +5 -27
- package/README.md +1 -1
- package/_tools/buildJs.js +28 -0
- package/_tools/cleanOutput.js +9 -13
- package/_tools/modules/updateOutputPath.js +36 -14
- package/_tools/modules/updatePage.js +31 -11
- package/_tools/res/templates/template.js +3 -11
- package/_tools/res/templates/template.ts +13 -0
- package/bin/create.js +161 -111
- package/docs/Assistant CLI.md +2 -0
- package/docs/Creating pages.md +2 -0
- package/docs/Javascript.md +30 -37
- package/package.json +8 -7
- package/src/frontend/js/modules/exampleModule.js +7 -0
- package/src/frontend/js/pages/404.js +3 -11
- package/src/frontend/js/pages/homepage.js +3 -11
- package/src/frontend/ts/modules/exampleModule.ts +3 -0
- package/src/frontend/ts/pages/404.ts +13 -0
- package/src/frontend/ts/pages/homepage.ts +13 -0
- package/src/frontend/js/modules/forms/normalizePhoneNumber.js +0 -42
- package/src/frontend/js/modules/forms/textAreaAutoExpand.js +0 -38
- package/src/frontend/js/modules/notification.js +0 -39
package/.eleventy.js
CHANGED
|
@@ -24,9 +24,6 @@ module.exports = function (eleventyConfig) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// =====================================================
|
|
28
|
-
// SHORTCODE — Markdown file renderer
|
|
29
|
-
// =====================================================
|
|
30
27
|
const md = markdownIt({ html: true });
|
|
31
28
|
|
|
32
29
|
eleventyConfig.addShortcode('mdFile', function(filePath) {
|
|
@@ -34,38 +31,21 @@ module.exports = function (eleventyConfig) {
|
|
|
34
31
|
return md.render(content);
|
|
35
32
|
});
|
|
36
33
|
|
|
37
|
-
// =====================================================
|
|
38
|
-
// SHORTCODE — Markdown css
|
|
39
|
-
// =====================================================
|
|
40
34
|
eleventyConfig.addPassthroughCopy({
|
|
41
35
|
"node_modules/github-markdown-css/github-markdown-dark.css": "css/github-markdown-dark.css",
|
|
42
36
|
"node_modules/github-markdown-css/github-markdown-light.css": "css/github-markdown-light.css",
|
|
43
37
|
});
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
// ESBUILD — Bundles and minifies JS files before build
|
|
47
|
-
// =====================================================
|
|
48
|
-
eleventyConfig.on("eleventy.before", async () => {
|
|
49
|
-
const entryPoints = glob.sync("src/frontend/js/pages/*.js");
|
|
50
|
-
await esbuild.build({
|
|
51
|
-
entryPoints,
|
|
52
|
-
bundle: true,
|
|
53
|
-
outdir: `${OUTPUT_DIR}/js/pages`,
|
|
54
|
-
minify: true,
|
|
55
|
-
});
|
|
39
|
+
eleventyConfig.on("eleventy.before", () => {
|
|
56
40
|
copyRecursiveSync("src/backend", `${OUTPUT_DIR}/backend`);
|
|
57
41
|
});
|
|
58
42
|
|
|
59
|
-
// =====================================================
|
|
60
|
-
// PASSTHROUGH — Static files
|
|
61
|
-
// =====================================================
|
|
62
43
|
eleventyConfig.addPassthroughCopy("src/frontend/.htaccess");
|
|
63
44
|
eleventyConfig.addPassthroughCopy("src/frontend/web.config");
|
|
64
45
|
eleventyConfig.addPassthroughCopy("src/frontend/assets");
|
|
65
46
|
eleventyConfig.addPassthroughCopy("src/frontend/robots.txt");
|
|
66
47
|
|
|
67
48
|
eleventyConfig.addPassthroughCopy({
|
|
68
|
-
// Bootstrap
|
|
69
49
|
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js": "js/bootstrap.bundle.min.js",
|
|
70
50
|
"node_modules/bootstrap-icons/font/fonts": "css/fonts",
|
|
71
51
|
|
|
@@ -79,9 +59,6 @@ module.exports = function (eleventyConfig) {
|
|
|
79
59
|
// Bulma — CSS only, no JS passthrough needed
|
|
80
60
|
});
|
|
81
61
|
|
|
82
|
-
// =====================================================
|
|
83
|
-
// ELEVENTY IMAGE — Responsive images
|
|
84
|
-
// =====================================================
|
|
85
62
|
eleventyConfig.addShortcode("image", async function (src, alt) {
|
|
86
63
|
let metadata = await Image(src, {
|
|
87
64
|
widths: [320, 480, 720, 1280, 1920, 2048, 2560, 3840, 4096, 7680],
|
|
@@ -98,11 +75,12 @@ module.exports = function (eleventyConfig) {
|
|
|
98
75
|
});
|
|
99
76
|
});
|
|
100
77
|
|
|
101
|
-
// =====================================================
|
|
102
|
-
// WATCH & DIRECTORY CONFIG
|
|
103
|
-
// =====================================================
|
|
104
78
|
eleventyConfig.addWatchTarget("./src/frontend/scss");
|
|
105
79
|
|
|
80
|
+
eleventyConfig.setServerOptions({
|
|
81
|
+
watch: [`${OUTPUT_DIR}/js/**/*.js`]
|
|
82
|
+
});
|
|
83
|
+
|
|
106
84
|
return {
|
|
107
85
|
dir: {
|
|
108
86
|
input: "src/frontend",
|
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
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const esbuild = require("esbuild");
|
|
2
|
+
const glob = require("glob");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const pkg = require("../package.json");
|
|
5
|
+
|
|
6
|
+
const isWatch = process.argv.includes("--watch");
|
|
7
|
+
const outputDir = pkg.outputDir || "out";
|
|
8
|
+
|
|
9
|
+
const jsFiles = glob.sync("src/frontend/js/pages/*.js");
|
|
10
|
+
const tsFiles = glob.sync("src/frontend/ts/pages/*.ts");
|
|
11
|
+
const entryPoints = [...jsFiles, ...tsFiles];
|
|
12
|
+
|
|
13
|
+
if (entryPoints.length === 0) {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const options = {
|
|
18
|
+
entryPoints,
|
|
19
|
+
bundle: true,
|
|
20
|
+
outdir: path.join(outputDir, "js/pages"),
|
|
21
|
+
minify: !isWatch,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (isWatch) {
|
|
25
|
+
esbuild.context(options).then(ctx => ctx.watch()).catch(() => process.exit(1));
|
|
26
|
+
} else {
|
|
27
|
+
esbuild.build(options).catch(() => process.exit(1));
|
|
28
|
+
}
|
package/_tools/cleanOutput.js
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const PACKAGE_JSON = path.resolve(__dirname, '../package.json');
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const match = content.match(/const OUTPUT_DIR\s*=\s*['"`]([^'"`]*)['"`]/);
|
|
6
|
+
const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf-8'));
|
|
8
7
|
|
|
9
|
-
if (!
|
|
10
|
-
console.log('(!)
|
|
8
|
+
if (!pkg.outputDir) {
|
|
9
|
+
console.log('(!) outputDir not found in package.json');
|
|
11
10
|
process.exit(1);
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
const outputDir =
|
|
15
|
-
const absPath = path.isAbsolute(outputDir)
|
|
16
|
-
? outputDir
|
|
17
|
-
: path.resolve(__dirname, '..', outputDir);
|
|
13
|
+
const outputDir = path.resolve(__dirname, '..', pkg.outputDir);
|
|
18
14
|
|
|
19
|
-
if (fs.existsSync(
|
|
20
|
-
fs.rmSync(
|
|
21
|
-
console.log(`(✓) cleaned → ${
|
|
15
|
+
if (fs.existsSync(outputDir)) {
|
|
16
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
17
|
+
console.log(`(✓) cleaned → ${outputDir}`);
|
|
22
18
|
} else {
|
|
23
|
-
console.log(`(i) nothing to clean → ${
|
|
19
|
+
console.log(`(i) nothing to clean → ${outputDir}`);
|
|
24
20
|
}
|
|
@@ -2,20 +2,22 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
const ELEVENTY_CONFIG = path.resolve(__dirname, '../../.eleventy.js');
|
|
5
|
-
const PACKAGE_JSON
|
|
5
|
+
const PACKAGE_JSON = path.resolve(__dirname, '../../package.json');
|
|
6
|
+
const TSCONFIG = path.resolve(__dirname, '../../tsconfig.json');
|
|
6
7
|
|
|
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
8
|
const OUTPUT_DIR_REGEX = /const OUTPUT_DIR\s*=\s*['"`]([^'"`]*)['"`]/;
|
|
10
9
|
|
|
11
10
|
// --- Helpers ---
|
|
12
11
|
|
|
13
|
-
// Reads OUTPUT_DIR value from a given file content string, or returns null
|
|
14
12
|
function parseOutputDir(content) {
|
|
15
13
|
const match = content.match(OUTPUT_DIR_REGEX);
|
|
16
14
|
return match ? match[1] : null;
|
|
17
15
|
}
|
|
18
16
|
|
|
17
|
+
function isTypeScriptProject() {
|
|
18
|
+
return fs.existsSync(TSCONFIG);
|
|
19
|
+
}
|
|
20
|
+
|
|
19
21
|
// --- Updaters ---
|
|
20
22
|
|
|
21
23
|
function updateEleventyConfig(newPath) {
|
|
@@ -39,35 +41,55 @@ function updateEleventyConfig(newPath) {
|
|
|
39
41
|
function updatePackageJson(newPath) {
|
|
40
42
|
const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf-8'));
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
pkg.outputDir = newPath;
|
|
45
|
+
|
|
46
|
+
const usesTs = isTypeScriptProject();
|
|
47
|
+
|
|
44
48
|
pkg.scripts['build:css'] = `sass src/frontend/scss:${newPath}/css --no-source-map --style=compressed --quiet`;
|
|
45
|
-
pkg.scripts['build:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --minify`;
|
|
46
49
|
pkg.scripts['serve:css'] = `sass --watch src/frontend/scss:${newPath}/css --no-source-map --quiet`;
|
|
47
|
-
|
|
50
|
+
|
|
51
|
+
if (usesTs) {
|
|
52
|
+
pkg.scripts['build:js'] = `esbuild "src/frontend/ts/pages/*.ts" --bundle --outdir=${newPath}/js/pages --minify`;
|
|
53
|
+
pkg.scripts['serve:js'] = `esbuild "src/frontend/ts/pages/*.ts" --bundle --outdir=${newPath}/js/pages --watch`;
|
|
54
|
+
} else {
|
|
55
|
+
pkg.scripts['build:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --minify`;
|
|
56
|
+
pkg.scripts['serve:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --watch`;
|
|
57
|
+
}
|
|
48
58
|
|
|
49
59
|
fs.writeFileSync(PACKAGE_JSON, JSON.stringify(pkg, null, 2), 'utf-8');
|
|
50
60
|
console.log(`(✓) package.json updated → ${newPath}`);
|
|
51
61
|
return true;
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
const TSCONFIG_OUTDIR_REGEX = /"outDir"\s*:\s*"[^"]*"/;
|
|
65
|
+
|
|
66
|
+
function updateTsConfig(newPath) {
|
|
67
|
+
if (!isTypeScriptProject()) return;
|
|
68
|
+
|
|
69
|
+
const content = fs.readFileSync(TSCONFIG, 'utf-8');
|
|
70
|
+
const updated = content.replace(TSCONFIG_OUTDIR_REGEX, `"outDir": "./${newPath}/ts"`);
|
|
71
|
+
|
|
72
|
+
if (content === updated) {
|
|
73
|
+
console.log('(!) outDir not found in tsconfig.json');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fs.writeFileSync(TSCONFIG, updated, 'utf-8');
|
|
78
|
+
console.log(`(✓) tsconfig.json updated → ${newPath}/ts`);
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
// --- Public API ---
|
|
55
82
|
|
|
56
83
|
function updateOutputPath(newPath) {
|
|
57
84
|
const trimmed = newPath.trim().replace(/\\/g, '/');
|
|
58
85
|
|
|
59
|
-
// Normalize the path: bare "." becomes "out", everything else gets a
|
|
60
|
-
// project-scoped suffix to avoid collisions
|
|
61
86
|
const normalizedPath = trimmed === '.'
|
|
62
87
|
? 'out'
|
|
63
88
|
: `${trimmed.replace(/\/$/, '')}/${path.basename(process.cwd())}-out`;
|
|
64
89
|
|
|
65
|
-
// Read the config once and reuse it to get the old path —
|
|
66
|
-
// avoids a second disk read inside updateEleventyConfig
|
|
67
90
|
const eleventyContent = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
|
|
68
91
|
const oldPath = parseOutputDir(eleventyContent);
|
|
69
92
|
|
|
70
|
-
// Delete the old output folder if it exists
|
|
71
93
|
if (oldPath) {
|
|
72
94
|
const oldAbsPath = path.resolve(__dirname, '../../', oldPath);
|
|
73
95
|
if (fs.existsSync(oldAbsPath)) {
|
|
@@ -82,9 +104,9 @@ function updateOutputPath(newPath) {
|
|
|
82
104
|
|
|
83
105
|
updatePackageJson(normalizedPath);
|
|
84
106
|
updateEleventyConfig(normalizedPath);
|
|
107
|
+
updateTsConfig(normalizedPath);
|
|
85
108
|
}
|
|
86
109
|
|
|
87
|
-
// Returns the current OUTPUT_DIR value from .eleventy.js, or null on failure
|
|
88
110
|
function getCurrentOutputPath() {
|
|
89
111
|
try {
|
|
90
112
|
const content = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
|
|
@@ -7,16 +7,35 @@ const { getCurrentOutputPath } = require('./updateOutputPath');
|
|
|
7
7
|
const { toCamelCase } = require('./utils');
|
|
8
8
|
|
|
9
9
|
const TEMPLATES_DIR = path.join(__dirname, '..', 'res', 'templates');
|
|
10
|
+
const TSCONFIG = path.resolve(__dirname, '../../tsconfig.json');
|
|
10
11
|
|
|
11
12
|
// --- Helpers ---
|
|
12
13
|
|
|
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
|
|
14
19
|
function getPageTargets(pageName) {
|
|
15
20
|
const camelName = toCamelCase(pageName);
|
|
21
|
+
const usesTs = isTypeScriptProject();
|
|
22
|
+
|
|
16
23
|
return [
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
{
|
|
25
|
+
folder: 'src/frontend/scss/pages',
|
|
26
|
+
templateFile: 'template.scss',
|
|
27
|
+
fileName: `${camelName}.scss`,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
folder: usesTs ? 'src/frontend/ts/pages' : 'src/frontend/js/pages',
|
|
31
|
+
templateFile: usesTs ? 'template.ts' : 'template.js',
|
|
32
|
+
fileName: usesTs ? `${camelName}.ts` : `${camelName}.js`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
folder: 'src/frontend/_routes',
|
|
36
|
+
templateFile: 'template.njk',
|
|
37
|
+
fileName: `${pageName}.njk`,
|
|
38
|
+
},
|
|
20
39
|
];
|
|
21
40
|
}
|
|
22
41
|
|
|
@@ -34,7 +53,6 @@ function addPage(pageName) {
|
|
|
34
53
|
const srcPath = path.join(TEMPLATES_DIR, templateFile);
|
|
35
54
|
|
|
36
55
|
if (templateFile === 'template.njk') {
|
|
37
|
-
// Patch the frontmatter placeholders with the actual page values
|
|
38
56
|
const content = fileSystem.readFileSync(srcPath, 'utf8')
|
|
39
57
|
.replace(/^title:.*$/m, `title: "${camelName}"`)
|
|
40
58
|
.replace(/^permalink:.*$/m, `permalink: "/${pageName}/"`);
|
|
@@ -53,11 +71,13 @@ function addPage(pageName) {
|
|
|
53
71
|
function renamePage(oldName, newName) {
|
|
54
72
|
const oldCamel = toCamelCase(oldName);
|
|
55
73
|
const newCamel = toCamelCase(newName);
|
|
74
|
+
const usesTs = isTypeScriptProject();
|
|
75
|
+
const ext = usesTs ? 'ts' : 'js';
|
|
76
|
+
const jsFolder = usesTs ? 'src/frontend/ts/pages' : 'src/frontend/js/pages';
|
|
56
77
|
|
|
57
|
-
// Use consistent src/frontend/ paths (matching addPage)
|
|
58
78
|
const filesToRename = [
|
|
59
79
|
{ src: `src/frontend/scss/pages/${oldCamel}.scss`, dest: `src/frontend/scss/pages/${newCamel}.scss` },
|
|
60
|
-
{ src:
|
|
80
|
+
{ src: `${jsFolder}/${oldCamel}.${ext}`, dest: `${jsFolder}/${newCamel}.${ext}` },
|
|
61
81
|
{ src: `src/frontend/_routes/${oldName}.njk`, dest: `src/frontend/_routes/${newName}.njk` },
|
|
62
82
|
];
|
|
63
83
|
|
|
@@ -70,20 +90,20 @@ function renamePage(oldName, newName) {
|
|
|
70
90
|
console.log(`[renamed] ${src} → ${dest}`);
|
|
71
91
|
});
|
|
72
92
|
|
|
73
|
-
// Use atomic rename helpers instead of remove + add separately
|
|
74
93
|
renameLayout(oldName, newName);
|
|
75
94
|
renameSiteData(oldName, newName);
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
function removePage(pageName) {
|
|
79
98
|
const camelName = toCamelCase(pageName);
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
const usesTs = isTypeScriptProject();
|
|
100
|
+
const ext = usesTs ? 'ts' : 'js';
|
|
101
|
+
const jsFolder = usesTs ? 'src/frontend/ts/pages' : 'src/frontend/js/pages';
|
|
82
102
|
const OUTPUT_DIR = getCurrentOutputPath() || 'out';
|
|
83
103
|
|
|
84
104
|
const filesToDelete = [
|
|
85
105
|
`src/frontend/scss/pages/${camelName}.scss`,
|
|
86
|
-
|
|
106
|
+
`${jsFolder}/${camelName}.${ext}`,
|
|
87
107
|
`src/frontend/_routes/${pageName}.njk`,
|
|
88
108
|
path.join(OUTPUT_DIR, 'js/pages', `${camelName}.js`),
|
|
89
109
|
path.join(OUTPUT_DIR, 'css/pages', `${camelName}.css`),
|
|
@@ -2,20 +2,12 @@
|
|
|
2
2
|
// JAVASCRIPT MODULES IMPORTS
|
|
3
3
|
//===========================
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
import { showNotification } from '../modules/notification.js';
|
|
7
|
-
|
|
8
|
-
// Uncomment of pre-existing modules
|
|
9
|
-
// import { initTextAreaAutoExpand } from '../modules/forms/textAreaAutoExpand.js';
|
|
10
|
-
// import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
|
|
5
|
+
// import { initExampleModule } from '../modules/exampleModule.js';
|
|
11
6
|
|
|
12
7
|
//==========================
|
|
13
8
|
// PAGE CUSTOM JAVASCRIPT
|
|
14
9
|
//==========================
|
|
15
10
|
|
|
16
11
|
document.addEventListener("DOMContentLoaded", () => {
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
showNotification("Example notification", "success", 3000);
|
|
12
|
+
// initExampleModule();
|
|
13
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//===========================
|
|
2
|
+
// TYPESCRIPT MODULES IMPORTS
|
|
3
|
+
//===========================
|
|
4
|
+
|
|
5
|
+
// import { initExampleModule } from '../modules/exampleModule';
|
|
6
|
+
|
|
7
|
+
//==========================
|
|
8
|
+
// PAGE CUSTOM TYPESCRIPT
|
|
9
|
+
//==========================
|
|
10
|
+
|
|
11
|
+
document.addEventListener("DOMContentLoaded", (): void => {
|
|
12
|
+
// initExampleModule();
|
|
13
|
+
});
|
package/bin/create.js
CHANGED
|
@@ -5,58 +5,87 @@ const path = require('path');
|
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
const { writeSync } = require('fs');
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// ── PATHS ────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const targetDir = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
|
|
9
11
|
const templateDir = path.join(__dirname, '..');
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
// ── ENUMS ────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const LANGUAGE = Object.freeze({
|
|
16
|
+
JAVASCRIPT: 'javascript',
|
|
17
|
+
TYPESCRIPT: 'typescript',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const FRAMEWORK = Object.freeze({
|
|
21
|
+
BOOTSTRAP: 'bootstrap',
|
|
22
|
+
BULMA: 'bulma',
|
|
23
|
+
FOUNDATION: 'foundation',
|
|
24
|
+
UIKIT: 'uikit',
|
|
25
|
+
NONE: 'none',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ── CHOICES ──────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const LANGUAGE_CHOICES = [
|
|
31
|
+
{ label: 'JavaScript (default)', value: LANGUAGE.JAVASCRIPT },
|
|
32
|
+
{ label: 'TypeScript', value: LANGUAGE.TYPESCRIPT },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const FRAMEWORK_CHOICES = [
|
|
36
|
+
{ label: 'Bootstrap (default)', value: FRAMEWORK.BOOTSTRAP },
|
|
37
|
+
{ label: 'Bulma', value: FRAMEWORK.BULMA },
|
|
38
|
+
{ label: 'Foundation', value: FRAMEWORK.FOUNDATION },
|
|
39
|
+
{ label: 'UIkit', value: FRAMEWORK.UIKIT },
|
|
40
|
+
{ label: 'None', value: FRAMEWORK.NONE },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// ── COPY CONFIG ───────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const MANDATORY_COPY = [
|
|
13
46
|
'docs',
|
|
14
47
|
'_tools',
|
|
15
48
|
'.eleventy.js',
|
|
16
49
|
'.eleventyignore',
|
|
50
|
+
'src/backend',
|
|
51
|
+
'src/frontend',
|
|
17
52
|
];
|
|
18
53
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
src/backend/config.php
|
|
24
|
-
`;
|
|
54
|
+
const FRONTEND_EXCLUDE = {
|
|
55
|
+
[LANGUAGE.JAVASCRIPT]: ['ts'],
|
|
56
|
+
[LANGUAGE.TYPESCRIPT]: ['js'],
|
|
57
|
+
};
|
|
25
58
|
|
|
26
59
|
const CREATE_DIRS = [
|
|
27
60
|
'src/frontend/_routes',
|
|
28
61
|
];
|
|
29
62
|
|
|
30
|
-
|
|
63
|
+
// ── FRAMEWORK CONFIG ──────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const ALL_FRAMEWORKS = Object.values(FRAMEWORK).filter(f => f !== FRAMEWORK.NONE);
|
|
31
66
|
|
|
32
67
|
const FRAMEWORKS = {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
njk: [
|
|
37
|
-
'<script src="/js/bootstrap.bundle.min.js" defer></script>',
|
|
38
|
-
],
|
|
68
|
+
[FRAMEWORK.BOOTSTRAP]: {
|
|
69
|
+
scss: 'bootstrap',
|
|
70
|
+
njk: ['<script src="/js/bootstrap.bundle.min.js" defer></script>'],
|
|
39
71
|
eleventy: [
|
|
40
72
|
'"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js": "js/bootstrap.bundle.min.js",',
|
|
41
73
|
'"node_modules/bootstrap-icons/font/fonts": "css/fonts",',
|
|
42
74
|
],
|
|
43
75
|
},
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
njk: [],
|
|
76
|
+
[FRAMEWORK.BULMA]: {
|
|
77
|
+
scss: 'bulma',
|
|
78
|
+
njk: [],
|
|
48
79
|
eleventy: [],
|
|
49
80
|
},
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
njk: ['<script src="/js/foundation.min.js" defer></script>'],
|
|
81
|
+
[FRAMEWORK.FOUNDATION]: {
|
|
82
|
+
scss: 'foundation',
|
|
83
|
+
njk: ['<script src="/js/foundation.min.js" defer></script>'],
|
|
54
84
|
eleventy: ['"node_modules/foundation-sites/dist/js/foundation.min.js": "js/foundation.min.js",'],
|
|
55
85
|
},
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
njk: [
|
|
86
|
+
[FRAMEWORK.UIKIT]: {
|
|
87
|
+
scss: 'uikit',
|
|
88
|
+
njk: [
|
|
60
89
|
'<script src="/js/uikit.min.js" defer></script>',
|
|
61
90
|
'<script src="/js/uikit-icons.min.js" defer></script>',
|
|
62
91
|
],
|
|
@@ -65,25 +94,42 @@ const FRAMEWORKS = {
|
|
|
65
94
|
'"node_modules/uikit/dist/js/uikit-icons.min.js": "js/uikit-icons.min.js",',
|
|
66
95
|
],
|
|
67
96
|
},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
njk: [],
|
|
97
|
+
[FRAMEWORK.NONE]: {
|
|
98
|
+
scss: null,
|
|
99
|
+
njk: [],
|
|
72
100
|
eleventy: [],
|
|
73
101
|
},
|
|
74
102
|
};
|
|
75
103
|
|
|
104
|
+
// ── LANGUAGE CONFIG ───────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const LANGUAGE_ELEVENTY = Object.freeze({
|
|
107
|
+
jsEntry: 'const entryPoints = glob.sync("src/frontend/js/pages/*.js");',
|
|
108
|
+
tsEntry: 'const entryPoints = glob.sync("src/frontend/ts/pages/*.ts");',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── GENERATED FILE CONTENTS ───────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
const GITIGNORE_CONTENT = `
|
|
114
|
+
node_modules/
|
|
115
|
+
src/backend/_core/vendor/
|
|
116
|
+
out/
|
|
117
|
+
src/backend/config.php
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
|
|
76
121
|
const PROJECT_PACKAGE = {
|
|
77
|
-
name:
|
|
78
|
-
version:
|
|
79
|
-
private:
|
|
80
|
-
|
|
122
|
+
name: path.basename(targetDir),
|
|
123
|
+
version: '2.2.0',
|
|
124
|
+
private: true,
|
|
125
|
+
outputDir: 'out',
|
|
126
|
+
"scripts": {
|
|
81
127
|
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet --load-path=node_modules",
|
|
82
|
-
"build:js": "
|
|
128
|
+
"build:js": "node _tools/buildJs.js",
|
|
83
129
|
"build:11ty": "eleventy",
|
|
84
130
|
"build": "npm run clean && npm run build:css && npm run build:js && npm run build:11ty",
|
|
85
131
|
"serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet --load-path=node_modules",
|
|
86
|
-
"serve:js": "
|
|
132
|
+
"serve:js": "node _tools/buildJs.js --watch",
|
|
87
133
|
"serve:11ty": "eleventy --serve --quiet",
|
|
88
134
|
"clean": "node _tools/cleanOutput.js",
|
|
89
135
|
"serve": "npm run clean && concurrently \"npm run serve:11ty\" \"npm run serve:css\" \"npm run serve:js\"",
|
|
@@ -91,23 +137,25 @@ const PROJECT_PACKAGE = {
|
|
|
91
137
|
"postinstall": "cd src/backend/_core && composer install --quiet"
|
|
92
138
|
},
|
|
93
139
|
dependencies: {
|
|
94
|
-
'@11ty/eleventy':
|
|
140
|
+
'@11ty/eleventy': '^3.1.2',
|
|
95
141
|
'@11ty/eleventy-img': '^6.0.4',
|
|
96
|
-
'bootstrap':
|
|
97
|
-
'bootstrap-icons':
|
|
98
|
-
'bulma':
|
|
99
|
-
'foundation-sites':
|
|
142
|
+
'bootstrap': '^5.3.8',
|
|
143
|
+
'bootstrap-icons': '^1.13.1',
|
|
144
|
+
'bulma': '^1.0.4',
|
|
145
|
+
'foundation-sites': '^6.9.0',
|
|
100
146
|
'github-markdown-css': '^5.9.0',
|
|
101
|
-
'glob':
|
|
102
|
-
'uikit':
|
|
147
|
+
'glob': '^13.0.6',
|
|
148
|
+
'uikit': '^3.25.13',
|
|
103
149
|
},
|
|
104
150
|
devDependencies: {
|
|
105
151
|
'concurrently': '^9.2.1',
|
|
106
|
-
'esbuild':
|
|
107
|
-
'sass':
|
|
152
|
+
'esbuild': '^0.27.3',
|
|
153
|
+
'sass': '^1.77.0',
|
|
108
154
|
},
|
|
109
155
|
};
|
|
110
156
|
|
|
157
|
+
// ── HELPERS ───────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
111
159
|
function log(msg) {
|
|
112
160
|
writeSync(1, msg + '\n');
|
|
113
161
|
}
|
|
@@ -116,13 +164,14 @@ function escapeRegex(str) {
|
|
|
116
164
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
117
165
|
}
|
|
118
166
|
|
|
119
|
-
function copyRecursive(src, dest) {
|
|
167
|
+
function copyRecursive(src, dest, exclude = []) {
|
|
120
168
|
const stat = fs.statSync(src);
|
|
121
169
|
if (stat.isDirectory()) {
|
|
122
170
|
fs.mkdirSync(dest, { recursive: true });
|
|
123
171
|
for (const child of fs.readdirSync(src)) {
|
|
124
172
|
if (child === '.git') continue;
|
|
125
|
-
|
|
173
|
+
if (exclude.includes(child)) continue;
|
|
174
|
+
copyRecursive(path.join(src, child), path.join(dest, child), exclude);
|
|
126
175
|
}
|
|
127
176
|
} else {
|
|
128
177
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
@@ -159,6 +208,8 @@ function njkUncomment(content, line) {
|
|
|
159
208
|
return content.split(`{# ${line} #}`).join(line);
|
|
160
209
|
}
|
|
161
210
|
|
|
211
|
+
// ── APPLY ─────────────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
162
213
|
function applyFramework(framework) {
|
|
163
214
|
const config = FRAMEWORKS[framework];
|
|
164
215
|
|
|
@@ -178,13 +229,9 @@ function applyFramework(framework) {
|
|
|
178
229
|
if (fs.existsSync(baseNjkPath)) {
|
|
179
230
|
let content = fs.readFileSync(baseNjkPath, 'utf8');
|
|
180
231
|
ALL_FRAMEWORKS.forEach(fw => {
|
|
181
|
-
FRAMEWORKS[fw].njk.forEach(line => {
|
|
182
|
-
content = njkComment(content, line);
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
config.njk.forEach(line => {
|
|
186
|
-
content = njkUncomment(content, line);
|
|
232
|
+
FRAMEWORKS[fw].njk.forEach(line => { content = njkComment(content, line); });
|
|
187
233
|
});
|
|
234
|
+
config.njk.forEach(line => { content = njkUncomment(content, line); });
|
|
188
235
|
fs.writeFileSync(baseNjkPath, content);
|
|
189
236
|
}
|
|
190
237
|
|
|
@@ -192,49 +239,50 @@ function applyFramework(framework) {
|
|
|
192
239
|
if (fs.existsSync(eleventyPath)) {
|
|
193
240
|
let content = fs.readFileSync(eleventyPath, 'utf8');
|
|
194
241
|
ALL_FRAMEWORKS.forEach(fw => {
|
|
195
|
-
FRAMEWORKS[fw].eleventy.forEach(line => {
|
|
196
|
-
content = slashComment(content, line);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
config.eleventy.forEach(line => {
|
|
200
|
-
content = slashUncomment(content, line);
|
|
242
|
+
FRAMEWORKS[fw].eleventy.forEach(line => { content = slashComment(content, line); });
|
|
201
243
|
});
|
|
244
|
+
config.eleventy.forEach(line => { content = slashUncomment(content, line); });
|
|
202
245
|
fs.writeFileSync(eleventyPath, content);
|
|
203
246
|
}
|
|
204
247
|
}
|
|
205
248
|
|
|
206
|
-
function
|
|
249
|
+
function applyLanguage(language) {
|
|
250
|
+
const eleventyPath = path.join(targetDir, '.eleventy.js');
|
|
251
|
+
if (!fs.existsSync(eleventyPath)) return;
|
|
252
|
+
|
|
253
|
+
let content = fs.readFileSync(eleventyPath, 'utf8');
|
|
254
|
+
|
|
255
|
+
if (language === LANGUAGE.TYPESCRIPT) {
|
|
256
|
+
content = slashComment(content, LANGUAGE_ELEVENTY.jsEntry);
|
|
257
|
+
content = slashUncomment(content, LANGUAGE_ELEVENTY.tsEntry);
|
|
258
|
+
} else {
|
|
259
|
+
content = slashUncomment(content, LANGUAGE_ELEVENTY.jsEntry);
|
|
260
|
+
content = slashComment(content, LANGUAGE_ELEVENTY.tsEntry);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fs.writeFileSync(eleventyPath, content);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── UI ────────────────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
function askChoice(question, choices) {
|
|
207
269
|
return new Promise((resolve) => {
|
|
208
|
-
const choices = [
|
|
209
|
-
{ label: 'Bootstrap (default)', value: 'bootstrap' },
|
|
210
|
-
{ label: 'Bulma', value: 'bulma' },
|
|
211
|
-
{ label: 'Foundation', value: 'foundation' },
|
|
212
|
-
{ label: 'UIkit', value: 'uikit' },
|
|
213
|
-
{ label: 'None', value: 'none' }
|
|
214
|
-
];
|
|
215
270
|
let selectedIndex = 0;
|
|
216
271
|
|
|
217
|
-
log(
|
|
272
|
+
log(`\n>> ${question} (Use arrow keys and press Enter):\n`);
|
|
218
273
|
|
|
219
274
|
const render = (firstTime = false) => {
|
|
220
|
-
if (!firstTime) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
output += ` \x1b[36m◉ ${choice.label}\x1b[0m\x1B[K\n`;
|
|
227
|
-
} else {
|
|
228
|
-
output += ` * ${choice.label}\x1B[K\n`;
|
|
229
|
-
}
|
|
230
|
-
});
|
|
275
|
+
if (!firstTime) process.stdout.write(`\x1B[${choices.length}A`);
|
|
276
|
+
const output = choices.map((choice, index) =>
|
|
277
|
+
index === selectedIndex
|
|
278
|
+
? ` \x1b[36m◉ ${choice.label}\x1b[0m\x1B[K\n`
|
|
279
|
+
: ` * ${choice.label}\x1B[K\n`
|
|
280
|
+
).join('');
|
|
231
281
|
process.stdout.write(output);
|
|
232
282
|
};
|
|
233
283
|
|
|
234
284
|
readline.emitKeypressEvents(process.stdin);
|
|
235
|
-
if (process.stdin.isTTY)
|
|
236
|
-
process.stdin.setRawMode(true);
|
|
237
|
-
}
|
|
285
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
238
286
|
process.stdin.resume();
|
|
239
287
|
|
|
240
288
|
const onKeyPress = (str, key) => {
|
|
@@ -248,9 +296,7 @@ function askFramework() {
|
|
|
248
296
|
render();
|
|
249
297
|
} else if (key.name === 'return' || key.name === 'enter') {
|
|
250
298
|
process.stdin.removeListener('keypress', onKeyPress);
|
|
251
|
-
if (process.stdin.isTTY)
|
|
252
|
-
process.stdin.setRawMode(false);
|
|
253
|
-
}
|
|
299
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
254
300
|
process.stdin.pause();
|
|
255
301
|
resolve(choices[selectedIndex].value);
|
|
256
302
|
}
|
|
@@ -261,23 +307,26 @@ function askFramework() {
|
|
|
261
307
|
});
|
|
262
308
|
}
|
|
263
309
|
|
|
310
|
+
// ── INIT ──────────────────────────────────────────────────────────────────────
|
|
311
|
+
|
|
264
312
|
async function init() {
|
|
265
|
-
if (!fs.existsSync(targetDir)) {
|
|
266
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
267
|
-
}
|
|
313
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
268
314
|
|
|
269
315
|
log(`\n>> Creating berna-stencil project in ${targetDir}\n`);
|
|
270
316
|
|
|
271
|
-
|
|
272
|
-
|
|
317
|
+
const language = await askChoice('Select a language', LANGUAGE_CHOICES);
|
|
318
|
+
const framework = await askChoice('Select a CSS framework', FRAMEWORK_CHOICES);
|
|
319
|
+
|
|
320
|
+
for (const target of MANDATORY_COPY) {
|
|
321
|
+
const src = path.join(templateDir, target);
|
|
273
322
|
const dest = path.join(targetDir, target);
|
|
274
|
-
if (fs.existsSync(src))
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
323
|
+
if (!fs.existsSync(src)) continue;
|
|
324
|
+
const exclude = target === 'src/frontend' ? FRONTEND_EXCLUDE[language] : [];
|
|
325
|
+
copyRecursive(src, dest, exclude);
|
|
326
|
+
log(`+ ${target}`);
|
|
278
327
|
}
|
|
279
328
|
|
|
280
|
-
const configDest
|
|
329
|
+
const configDest = path.join(targetDir, 'src/backend/config.php');
|
|
281
330
|
const configExample = path.join(targetDir, 'src/backend/config.example.php');
|
|
282
331
|
if (!fs.existsSync(configDest) && fs.existsSync(configExample)) {
|
|
283
332
|
fs.copyFileSync(configExample, configDest);
|
|
@@ -285,30 +334,31 @@ async function init() {
|
|
|
285
334
|
}
|
|
286
335
|
deleteFileRecursive(targetDir, 'config.example.php');
|
|
287
336
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
337
|
+
const pkg = { ...PROJECT_PACKAGE };
|
|
338
|
+
|
|
339
|
+
if (language === LANGUAGE.TYPESCRIPT) {
|
|
340
|
+
const tsSrc = path.join(templateDir, 'tsconfig.json');
|
|
341
|
+
const tsDest = path.join(targetDir, 'tsconfig.json');
|
|
342
|
+
fs.copyFileSync(tsSrc, tsDest);
|
|
343
|
+
log('+ tsconfig.json');
|
|
344
|
+
pkg.devDependencies = { ...pkg.devDependencies, typescript: 'latest' };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
292
348
|
log('+ package.json');
|
|
293
349
|
|
|
294
|
-
fs.writeFileSync(
|
|
295
|
-
path.join(targetDir, '.gitignore'),
|
|
296
|
-
GITIGNORE_CONTENT
|
|
297
|
-
);
|
|
350
|
+
fs.writeFileSync(path.join(targetDir, '.gitignore'), GITIGNORE_CONTENT);
|
|
298
351
|
log('+ .gitignore');
|
|
299
352
|
|
|
300
353
|
for (const dir of CREATE_DIRS) {
|
|
301
|
-
|
|
302
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
354
|
+
fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
|
|
303
355
|
}
|
|
304
356
|
|
|
305
|
-
const framework = await askFramework();
|
|
306
357
|
applyFramework(framework);
|
|
358
|
+
applyLanguage(language);
|
|
307
359
|
|
|
308
360
|
log(`\n>> Done! Now run:\n`);
|
|
309
|
-
if (process.argv[2]) {
|
|
310
|
-
log(`cd ${process.argv[2]}`);
|
|
311
|
-
}
|
|
361
|
+
if (process.argv[2]) log(`cd ${process.argv[2]}`);
|
|
312
362
|
log('npm install');
|
|
313
363
|
log('npm run serve\n');
|
|
314
364
|
}
|
package/docs/Assistant CLI.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Assistant CLI
|
|
2
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
|
+
|
|
3
5
|
An interactive CLI to manage pages without touching files manually.
|
|
4
6
|
|
|
5
7
|
```
|
package/docs/Creating pages.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Creating Pages
|
|
2
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
|
+
|
|
3
5
|
The recommended way is via the **Assistant CLI**
|
|
4
6
|
|
|
5
7
|
## What gets created
|
package/docs/Javascript.md
CHANGED
|
@@ -1,70 +1,63 @@
|
|
|
1
1
|
# JavaScript
|
|
2
2
|
|
|
3
|
-
|
|
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
4
|
|
|
5
|
-
|
|
5
|
+
## Page JS
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Each page has its own JS entry point in `src/frontend/js/pages/`, bundled and minified by esbuild and loaded automatically by `base.njk`.
|
|
8
8
|
|
|
9
9
|
Import only what the page needs.
|
|
10
10
|
|
|
11
11
|
### examplePage.js <small>(`src/frontend/js/pages/`)</small>
|
|
12
|
+
|
|
12
13
|
```js
|
|
13
|
-
|
|
14
|
+
//===========================
|
|
15
|
+
// JAVASCRIPT MODULES IMPORTS
|
|
16
|
+
//===========================
|
|
17
|
+
|
|
18
|
+
// import { initExampleModule } from '../modules/exampleModule.js';
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
//==========================
|
|
21
|
+
// PAGE CUSTOM JAVASCRIPT
|
|
22
|
+
//==========================
|
|
16
23
|
|
|
17
24
|
document.addEventListener("DOMContentLoaded", () => {
|
|
18
|
-
|
|
25
|
+
// initExampleModule();
|
|
19
26
|
});
|
|
20
|
-
|
|
21
|
-
showNotification("Page loaded", "success", 3000);
|
|
22
27
|
```
|
|
23
28
|
|
|
24
29
|
## Modules
|
|
25
30
|
|
|
26
|
-
Modules live in `src/frontend/js/modules/`.
|
|
27
|
-
|
|
28
|
-
### Call inside `DOMContentLoaded`
|
|
29
|
-
|
|
30
|
-
| Module | Function |
|
|
31
|
-
|---|---|
|
|
32
|
-
| `modules/langSwitcher.js` | `initLangSwitcher()` |
|
|
33
|
-
| `modules/forms/form.js` | `initFormListener()` |
|
|
34
|
-
| `modules/forms/textAreaAutoExpand.js` | `initTextAreaAutoExpand()` |
|
|
35
|
-
| `modules/forms/normalizePhoneNumber.js` | `initNormalizePhoneNumber()` |
|
|
36
|
-
|
|
37
|
-
### Call anywhere
|
|
38
|
-
|
|
39
|
-
| Module | Function |
|
|
40
|
-
|---|---|
|
|
41
|
-
| `modules/notification.js` | `showNotification(text, type, duration)` |
|
|
42
|
-
|
|
43
|
-
### `showNotification` parameters
|
|
44
|
-
|
|
45
|
-
| Parameter | Type | Default | Values |
|
|
46
|
-
|---|---|---|---|
|
|
47
|
-
| `text` | string | — | Any string |
|
|
48
|
-
| `type` | string | `"info"` | `"success"`, `"info"`, `"error"` |
|
|
49
|
-
| `duration` | number | `5000` | ms, or `-1` for persistent with spinner |
|
|
31
|
+
Modules live in `src/frontend/js/modules/`. Modules that interact with the DOM must be called inside `DOMContentLoaded`; others can be called anywhere.
|
|
50
32
|
|
|
51
33
|
## Adding a module
|
|
52
34
|
|
|
53
|
-
Create a new `.js` file in `src/frontend/js/modules/`.
|
|
35
|
+
Create a new `.js` file in `src/frontend/js/modules/`. Subfolders are allowed.
|
|
54
36
|
|
|
55
37
|
Use ESM syntax — esbuild handles the bundling:
|
|
56
38
|
|
|
39
|
+
### exampleModule.js <small>(`src/frontend/js/modules/`)</small>
|
|
40
|
+
|
|
57
41
|
```js
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
42
|
+
//==========================
|
|
43
|
+
// EXAMPLE MODULE
|
|
44
|
+
//==========================
|
|
45
|
+
|
|
46
|
+
export function exampleModule() {
|
|
47
|
+
// Example module logic
|
|
61
48
|
}
|
|
62
49
|
```
|
|
63
50
|
|
|
64
51
|
Then import it in the pages that need it:
|
|
65
52
|
|
|
66
53
|
```js
|
|
67
|
-
import {
|
|
54
|
+
import { exampleModule } from '../modules/exampleModule.js';
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
In TypeScript, omit the extension:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { exampleModule } from '../modules/exampleModule';
|
|
68
61
|
```
|
|
69
62
|
|
|
70
63
|
> ⚠️ Files inside `_tools/` run directly in Node.js without a bundler — use CommonJS (`require` / `module.exports`) there, not ESM.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-berna-stencil",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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",
|
|
@@ -47,17 +47,18 @@
|
|
|
47
47
|
"esbuild": "^0.27.3",
|
|
48
48
|
"sass": "^1.77.0"
|
|
49
49
|
},
|
|
50
|
-
|
|
51
|
-
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet",
|
|
52
|
-
"build:js": "
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet --load-path=node_modules",
|
|
52
|
+
"build:js": "node _tools/buildJs.js",
|
|
53
53
|
"build:11ty": "eleventy",
|
|
54
54
|
"build": "npm run clean && npm run build:css && npm run build:js && npm run build:11ty",
|
|
55
|
-
"serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet",
|
|
56
|
-
"serve:js": "
|
|
55
|
+
"serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet --load-path=node_modules",
|
|
56
|
+
"serve:js": "node _tools/buildJs.js --watch",
|
|
57
57
|
"serve:11ty": "eleventy --serve --quiet",
|
|
58
58
|
"clean": "node _tools/cleanOutput.js",
|
|
59
59
|
"serve": "npm run clean && concurrently \"npm run serve:11ty\" \"npm run serve:css\" \"npm run serve:js\"",
|
|
60
60
|
"assistant": "node _tools/assistant.js",
|
|
61
61
|
"postinstall": "cd src/backend/_core && composer install --quiet"
|
|
62
|
-
}
|
|
62
|
+
},
|
|
63
|
+
"outputDir": "out"
|
|
63
64
|
}
|
|
@@ -2,20 +2,12 @@
|
|
|
2
2
|
// JAVASCRIPT MODULES IMPORTS
|
|
3
3
|
//===========================
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
import { showNotification } from '../modules/notification.js';
|
|
7
|
-
|
|
8
|
-
// Uncomment of pre-existing modules
|
|
9
|
-
// import { initTextAreaAutoExpand } from '../modules/forms/textAreaAutoExpand.js';
|
|
10
|
-
// import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
|
|
5
|
+
// import { initExampleModule } from '../modules/exampleModule.js';
|
|
11
6
|
|
|
12
7
|
//==========================
|
|
13
8
|
// PAGE CUSTOM JAVASCRIPT
|
|
14
9
|
//==========================
|
|
15
10
|
|
|
16
11
|
document.addEventListener("DOMContentLoaded", () => {
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
showNotification("Example notification", "success", 3000);
|
|
12
|
+
// initExampleModule();
|
|
13
|
+
});
|
|
@@ -2,20 +2,12 @@
|
|
|
2
2
|
// JAVASCRIPT MODULES IMPORTS
|
|
3
3
|
//===========================
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
import { showNotification } from '../modules/notification.js';
|
|
7
|
-
|
|
8
|
-
// Uncomment of pre-existing modules
|
|
9
|
-
// import { initTextAreaAutoExpand } from '../modules/forms/textAreaAutoExpand.js';
|
|
10
|
-
// import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
|
|
5
|
+
// import { initExampleModule } from '../modules/exampleModule.js';
|
|
11
6
|
|
|
12
7
|
//==========================
|
|
13
8
|
// PAGE CUSTOM JAVASCRIPT
|
|
14
9
|
//==========================
|
|
15
10
|
|
|
16
11
|
document.addEventListener("DOMContentLoaded", () => {
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
showNotification("Example notification", "success", 3000);
|
|
12
|
+
// initExampleModule();
|
|
13
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// ===========================
|
|
2
|
+
// TYPESCRIPT MODULES IMPORTS
|
|
3
|
+
// ===========================
|
|
4
|
+
|
|
5
|
+
// import { initExampleModule } from '../modules/exampleModule';
|
|
6
|
+
|
|
7
|
+
// ==========================
|
|
8
|
+
// PAGE CUSTOM TYPESCRIPT
|
|
9
|
+
// ==========================
|
|
10
|
+
|
|
11
|
+
document.addEventListener("DOMContentLoaded", (): void => {
|
|
12
|
+
// initExampleModule();
|
|
13
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// ===========================
|
|
2
|
+
// TYPESCRIPT MODULES IMPORTS
|
|
3
|
+
// ===========================
|
|
4
|
+
|
|
5
|
+
// import { initExampleModule } from '../modules/exampleModule';
|
|
6
|
+
|
|
7
|
+
// ==========================
|
|
8
|
+
// PAGE CUSTOM TYPESCRIPT
|
|
9
|
+
// ==========================
|
|
10
|
+
|
|
11
|
+
document.addEventListener("DOMContentLoaded", (): void => {
|
|
12
|
+
// initExampleModule();
|
|
13
|
+
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
//==========================
|
|
2
|
-
// PHONE NUMBER NORMALIZATION MODULE
|
|
3
|
-
//==========================
|
|
4
|
-
|
|
5
|
-
// Normalizes and attaches normalization to all existing and future tel inputs
|
|
6
|
-
// Must be called inside DOMContentLoaded in the page's JS file
|
|
7
|
-
export function initNormalizePhoneNumber() {
|
|
8
|
-
|
|
9
|
-
// Formats digits in groups of 3, 3 and 4 (e.g. 333 123 4567)
|
|
10
|
-
function formatDigits(digits) {
|
|
11
|
-
let formatted = "";
|
|
12
|
-
if (digits.length > 0) formatted += digits.slice(0, 3);
|
|
13
|
-
if (digits.length > 3) formatted += " " + digits.slice(3, 6);
|
|
14
|
-
if (digits.length > 6) formatted += " " + digits.slice(6, 10);
|
|
15
|
-
return formatted;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function normalize(input) {
|
|
19
|
-
if (!input) return;
|
|
20
|
-
|
|
21
|
-
let value = input.value.replace(/[^0-9+]/g, "");
|
|
22
|
-
|
|
23
|
-
if (value.startsWith("+")) {
|
|
24
|
-
value = "+" + value.slice(1).replace(/\+/g, "");
|
|
25
|
-
const prefix = value.slice(0, 3);
|
|
26
|
-
const digits = value.slice(3);
|
|
27
|
-
|
|
28
|
-
value = prefix + (digits ? " " + formatDigits(digits) : "");
|
|
29
|
-
} else {
|
|
30
|
-
value = value.replace(/\+/g, "");
|
|
31
|
-
value = formatDigits(value);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
input.value = value;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
document.querySelectorAll('input[type="tel"]').forEach(normalize);
|
|
38
|
-
|
|
39
|
-
document.addEventListener("input", ({ target }) => {
|
|
40
|
-
if (target.matches('input[type="tel"]')) normalize(target);
|
|
41
|
-
});
|
|
42
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
//==========================
|
|
2
|
-
// AUTO-EXPAND TEXTAREA MODULE
|
|
3
|
-
//==========================
|
|
4
|
-
|
|
5
|
-
export function initTextAreaAutoExpand() {
|
|
6
|
-
const MAX_ROWS = 10;
|
|
7
|
-
|
|
8
|
-
function setup(element) {
|
|
9
|
-
if (element.dataset.autoExpand) return;
|
|
10
|
-
element.dataset.autoExpand = "true";
|
|
11
|
-
element.style.resize = "none";
|
|
12
|
-
element.style.overflow = "hidden";
|
|
13
|
-
element.dataset.minRows = element.rows || 1;
|
|
14
|
-
element.addEventListener("input", () => expand(element));
|
|
15
|
-
if (element.value) expand(element);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function expand(element) {
|
|
19
|
-
element.rows = element.dataset.minRows;
|
|
20
|
-
while (element.scrollHeight > element.clientHeight && element.rows < MAX_ROWS) {
|
|
21
|
-
element.rows += 1;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
document.querySelectorAll("textarea").forEach(setup);
|
|
26
|
-
|
|
27
|
-
const observer = new MutationObserver((mutations) => {
|
|
28
|
-
for (const mutation of mutations) {
|
|
29
|
-
for (const node of mutation.addedNodes) {
|
|
30
|
-
if (node.nodeType !== 1) continue;
|
|
31
|
-
if (node.matches("textarea")) setup(node);
|
|
32
|
-
node.querySelectorAll?.("textarea").forEach(setup);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
38
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
//==========================
|
|
2
|
-
// NOTIFICATION MODULE
|
|
3
|
-
//==========================
|
|
4
|
-
|
|
5
|
-
let currentNotification = null;
|
|
6
|
-
|
|
7
|
-
export function showNotification(text, type = "info", duration = 5000) {
|
|
8
|
-
if (currentNotification) {
|
|
9
|
-
currentNotification.remove();
|
|
10
|
-
currentNotification = null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const notificationBox = document.createElement("div");
|
|
14
|
-
notificationBox.classList.add("notificationBox", type);
|
|
15
|
-
|
|
16
|
-
const notificationText = document.createElement("span");
|
|
17
|
-
notificationText.textContent = text;
|
|
18
|
-
notificationBox.appendChild(notificationText);
|
|
19
|
-
|
|
20
|
-
if (duration === -1) {
|
|
21
|
-
const spinner = document.createElement("div");
|
|
22
|
-
spinner.classList.add("spinner");
|
|
23
|
-
notificationBox.appendChild(spinner);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
document.body.appendChild(notificationBox);
|
|
27
|
-
currentNotification = notificationBox;
|
|
28
|
-
|
|
29
|
-
if (duration !== -1) {
|
|
30
|
-
setTimeout(() => {
|
|
31
|
-
if (!notificationBox.parentElement) return;
|
|
32
|
-
notificationBox.classList.add("fade-out");
|
|
33
|
-
notificationBox.addEventListener("transitionend", () => {
|
|
34
|
-
notificationBox.remove();
|
|
35
|
-
if (currentNotification === notificationBox) currentNotification = null;
|
|
36
|
-
}, { once: true });
|
|
37
|
-
}, duration);
|
|
38
|
-
}
|
|
39
|
-
}
|