nibula 1.2.4 → 1.2.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/.eleventy.js +81 -81
- package/.eleventyignore +3 -3
- package/.github/workflows/publish.yml +37 -0
- package/CHANGELOG.md +166 -148
- package/LICENSE +169 -169
- package/NOTICE +4 -4
- package/README.md +120 -123
- package/bin/create.js +507 -507
- package/bin/nibula.js +317 -305
- package/docs/Assistant CLI.md +65 -65
- package/docs/Backend.md +295 -295
- package/docs/Components.md +84 -84
- package/docs/Creating pages.md +64 -64
- package/docs/Deploy.md +189 -189
- package/docs/Head and SEO.md +138 -138
- package/docs/Javascript.md +50 -50
- package/docs/Styling with SCSS.md +141 -141
- package/nginx.conf +75 -75
- package/package.json +1 -1
- package/src/backend/.htaccess +6 -6
- package/src/backend/_core/composer.json +5 -5
- package/src/backend/_core/composer.lock +492 -492
- package/src/backend/_core/index.js +267 -267
- package/src/backend/_core/index.php +147 -147
- package/src/backend/_core/init.js +52 -52
- package/src/backend/_core/init.php +33 -33
- package/src/backend/_core/modules/RateLimiter.js +58 -58
- package/src/backend/_core/modules/RateLimiter.php +30 -30
- package/src/backend/_core/modules/Response.js +59 -59
- package/src/backend/_core/modules/Response.php +48 -48
- package/src/backend/api/protected/example-protected.js +23 -23
- package/src/backend/api/protected/example-protected.php +16 -16
- package/src/backend/api/public/example-public.js +24 -24
- package/src/backend/api/public/example-public.php +16 -16
- package/src/backend/backend-node.service.example +30 -30
- package/src/backend/database/Database.js +46 -46
- package/src/backend/database/Database.php +23 -23
- package/src/backend/example.config.js +37 -37
- package/src/backend/example.config.php +27 -27
- package/src/backend/package.json +18 -18
- package/src/backend/web.config +16 -16
- package/src/frontend/.htaccess +51 -51
- package/src/frontend/404.njk +17 -17
- package/src/frontend/assets/brand/favicon.svg +54 -54
- package/src/frontend/assets/brand/logo.svg +54 -54
- package/src/frontend/components/global/footer.njk +25 -25
- package/src/frontend/components/global/header.njk +6 -6
- package/src/frontend/components/welcome.njk +114 -114
- package/src/frontend/data/site.json +48 -48
- package/src/frontend/index.njk +8 -8
- package/src/frontend/js/modules/exampleModule.js +2 -2
- package/src/frontend/js/pages/404.js +6 -6
- package/src/frontend/js/pages/homepage.js +6 -6
- package/src/frontend/layouts/base.njk +132 -132
- package/src/frontend/layouts/page-components.njk +13 -13
- package/src/frontend/llms.njk +17 -17
- package/src/frontend/robots.njk +7 -7
- package/src/frontend/scss/modules/_animations.scss +24 -24
- package/src/frontend/scss/modules/_footer.scss +27 -27
- package/src/frontend/scss/modules/_global.scss +47 -47
- package/src/frontend/scss/modules/_header.scss +27 -27
- package/src/frontend/scss/modules/_mobile.scss +29 -29
- package/src/frontend/scss/modules/_root.scss +34 -34
- package/src/frontend/scss/modules/_typography.scss +14 -14
- package/src/frontend/scss/modules/frameworks/_bootstrap.scss +109 -109
- package/src/frontend/scss/modules/frameworks/_bulma.scss +108 -108
- package/src/frontend/scss/modules/frameworks/_foundation.scss +138 -139
- package/src/frontend/scss/modules/frameworks/_uikit.scss +109 -109
- package/src/frontend/scss/pages/404.scss +27 -27
- package/src/frontend/scss/pages/homepage.scss +22 -22
- package/src/frontend/sitemap.njk +17 -17
- package/src/frontend/ts/modules/exampleModule.ts +2 -2
- package/src/frontend/ts/pages/404.ts +6 -6
- package/src/frontend/ts/pages/homepage.ts +6 -6
- package/src/frontend/web.config +55 -55
- package/tools/config/messages.json +3 -1
- package/tools/res/templates/template.js +6 -6
- package/tools/res/templates/template.njk +8 -8
- package/tools/res/templates/template.scss +22 -22
- package/tools/res/templates/template.ts +6 -6
- package/tsconfig.json +24 -24
package/bin/create.js
CHANGED
|
@@ -1,508 +1,508 @@
|
|
|
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
|
-
const { color } = require('../tools/lib/colors');
|
|
9
|
-
|
|
10
|
-
// ── PATHS ────────────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
const targetDir = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
|
|
13
|
-
const templateDir = path.join(__dirname, '..');
|
|
14
|
-
const SELF_VERSION = require('../package.json').version;
|
|
15
|
-
|
|
16
|
-
// ── ENUMS ────────────────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
const LANGUAGE = Object.freeze({
|
|
19
|
-
JAVASCRIPT: 'javascript',
|
|
20
|
-
TYPESCRIPT: 'typescript',
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const FRAMEWORK = Object.freeze({
|
|
24
|
-
BOOTSTRAP: 'bootstrap',
|
|
25
|
-
BULMA: 'bulma',
|
|
26
|
-
FOUNDATION: 'foundation',
|
|
27
|
-
UIKIT: 'uikit',
|
|
28
|
-
NONE: 'none',
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const BACKEND = Object.freeze({
|
|
32
|
-
NODE: 'node',
|
|
33
|
-
PHP: 'php',
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// ── CHOICES ──────────────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
const LANGUAGE_CHOICES = [
|
|
39
|
-
{ label: 'JavaScript (
|
|
40
|
-
{ label: 'TypeScript', value: LANGUAGE.TYPESCRIPT },
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
const FRAMEWORK_CHOICES = [
|
|
44
|
-
{ label: 'Bootstrap (
|
|
45
|
-
{ label: 'Bulma', value: FRAMEWORK.BULMA },
|
|
46
|
-
{ label: 'Foundation', value: FRAMEWORK.FOUNDATION },
|
|
47
|
-
{ label: 'UIkit', value: FRAMEWORK.UIKIT },
|
|
48
|
-
{ label: 'None', value: FRAMEWORK.NONE },
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
const BACKEND_CHOICES = [
|
|
52
|
-
{ label: 'Node.js (
|
|
53
|
-
{ label: 'PHP (
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
// Runtime dependency added to the ROOT package.json when the Node backend is
|
|
57
|
-
// chosen, so it lands in the root node_modules (never in src/backend).
|
|
58
|
-
const NODE_BACKEND_DEPENDENCIES = {
|
|
59
|
-
mysql2: '^3.11.0',
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// ── COPY CONFIG ───────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
const MANDATORY_COPY = [
|
|
65
|
-
'.eleventy.js',
|
|
66
|
-
'.eleventyignore',
|
|
67
|
-
'nginx.conf',
|
|
68
|
-
'src/backend',
|
|
69
|
-
'src/frontend',
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
const FRONTEND_EXCLUDE = {
|
|
73
|
-
[LANGUAGE.JAVASCRIPT]: ['ts'],
|
|
74
|
-
[LANGUAGE.TYPESCRIPT]: ['js'],
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const CREATE_DIRS = [
|
|
78
|
-
'src/frontend/_routes',
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
// Backend files that belong to exactly one backend, matched by basename.
|
|
82
|
-
// Everything else (migrations, .htaccess, web.config, README, ...) is shared.
|
|
83
|
-
const NODE_ONLY_FILES = new Set(['package.json', 'backend-node.service.example']);
|
|
84
|
-
const PHP_ONLY_FILES = new Set(['composer.json', 'composer.lock']);
|
|
85
|
-
const PHP_ONLY_DIRS = new Set(['vendor']);
|
|
86
|
-
// Runtime artifacts that must never be copied from the template, either way.
|
|
87
|
-
const BACKEND_SKIP_DIRS = new Set(['node_modules', 'cache', '.git']);
|
|
88
|
-
|
|
89
|
-
// ── FRAMEWORK CONFIG ──────────────────────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
const ALL_FRAMEWORKS = Object.values(FRAMEWORK).filter(f => f !== FRAMEWORK.NONE);
|
|
92
|
-
|
|
93
|
-
const FRAMEWORKS = {
|
|
94
|
-
[FRAMEWORK.BOOTSTRAP]: {
|
|
95
|
-
scss: 'bootstrap',
|
|
96
|
-
njk: ['<script src="/js/bootstrap.bundle.min.js" defer></script>'],
|
|
97
|
-
eleventy: [
|
|
98
|
-
'"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js": "js/bootstrap.bundle.min.js",',
|
|
99
|
-
'"node_modules/bootstrap-icons/font/fonts": "css/fonts",',
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
|
-
[FRAMEWORK.BULMA]: {
|
|
103
|
-
scss: 'bulma',
|
|
104
|
-
njk: [],
|
|
105
|
-
eleventy: [],
|
|
106
|
-
},
|
|
107
|
-
[FRAMEWORK.FOUNDATION]: {
|
|
108
|
-
scss: 'foundation',
|
|
109
|
-
njk: ['<script src="/js/foundation.min.js" defer></script>'],
|
|
110
|
-
eleventy: ['"node_modules/foundation-sites/dist/js/foundation.min.js": "js/foundation.min.js",'],
|
|
111
|
-
},
|
|
112
|
-
[FRAMEWORK.UIKIT]: {
|
|
113
|
-
scss: 'uikit',
|
|
114
|
-
njk: [
|
|
115
|
-
'<script src="/js/uikit.min.js" defer></script>',
|
|
116
|
-
'<script src="/js/uikit-icons.min.js" defer></script>',
|
|
117
|
-
],
|
|
118
|
-
eleventy: [
|
|
119
|
-
'"node_modules/uikit/dist/js/uikit.min.js": "js/uikit.min.js",',
|
|
120
|
-
'"node_modules/uikit/dist/js/uikit-icons.min.js": "js/uikit-icons.min.js",',
|
|
121
|
-
],
|
|
122
|
-
},
|
|
123
|
-
[FRAMEWORK.NONE]: {
|
|
124
|
-
scss: null,
|
|
125
|
-
njk: [],
|
|
126
|
-
eleventy: [],
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
// ── LANGUAGE CONFIG ───────────────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
const LANGUAGE_ELEVENTY = Object.freeze({
|
|
133
|
-
jsEntry: 'const entryPoints = glob.sync("src/frontend/js/pages/*.js");',
|
|
134
|
-
tsEntry: 'const entryPoints = glob.sync("src/frontend/ts/pages/*.ts");',
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// ── GENERATED FILE CONTENTS ───────────────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
const GITIGNORE_CONTENT = `
|
|
140
|
-
node_modules/
|
|
141
|
-
src/backend/_core/vendor/
|
|
142
|
-
out/
|
|
143
|
-
src/backend/config.php
|
|
144
|
-
src/backend/config.js
|
|
145
|
-
src/backend/cache/
|
|
146
|
-
`;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const PROJECT_PACKAGE = {
|
|
150
|
-
name: path.basename(targetDir),
|
|
151
|
-
version: '0.0.0',
|
|
152
|
-
private: true,
|
|
153
|
-
outputDir: 'out',
|
|
154
|
-
"scripts": {
|
|
155
|
-
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet --load-path=node_modules",
|
|
156
|
-
"build:js": "nib build-js",
|
|
157
|
-
"build:11ty": "eleventy",
|
|
158
|
-
"build": "npm run clean && npm run build:css && npm run build:js && npm run build:11ty",
|
|
159
|
-
"serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet --load-path=node_modules",
|
|
160
|
-
"serve:js": "nib build-js --watch",
|
|
161
|
-
"serve:11ty": "eleventy --serve --quiet",
|
|
162
|
-
"clean": "nib clean",
|
|
163
|
-
"serve": "npm run clean && concurrently \"npm run serve:11ty\" \"npm run serve:css\" \"npm run serve:js\"",
|
|
164
|
-
"assistant": "nib cli"
|
|
165
|
-
},
|
|
166
|
-
dependencies: {
|
|
167
|
-
'@11ty/eleventy': '^3.1.2',
|
|
168
|
-
'@11ty/eleventy-img': '^6.0.4',
|
|
169
|
-
'bootstrap': '^5.3.8',
|
|
170
|
-
'bootstrap-icons': '^1.13.1',
|
|
171
|
-
'bulma': '^1.0.4',
|
|
172
|
-
'foundation-sites': '^6.9.0',
|
|
173
|
-
'glob': '^13.0.6',
|
|
174
|
-
'uikit': '^3.25.13',
|
|
175
|
-
},
|
|
176
|
-
devDependencies: {
|
|
177
|
-
'nibula': `^${SELF_VERSION}`,
|
|
178
|
-
'concurrently': '^9.2.1',
|
|
179
|
-
'esbuild': '^0.27.3',
|
|
180
|
-
'sass': '^1.77.0',
|
|
181
|
-
},
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
// ── HELPERS ───────────────────────────────────────────────────────────────────
|
|
185
|
-
|
|
186
|
-
function log(msg) {
|
|
187
|
-
writeSync(1, msg + '\n');
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function logAdd(name) {
|
|
191
|
-
log(`${color.green}+${color.reset} ${name}`);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function escapeRegex(str) {
|
|
195
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function copyRecursive(src, dest, exclude = []) {
|
|
199
|
-
const stat = fs.statSync(src);
|
|
200
|
-
if (stat.isDirectory()) {
|
|
201
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
202
|
-
for (const child of fs.readdirSync(src)) {
|
|
203
|
-
if (child === '.git') continue;
|
|
204
|
-
if (exclude.includes(child)) continue;
|
|
205
|
-
copyRecursive(path.join(src, child), path.join(dest, child), exclude);
|
|
206
|
-
}
|
|
207
|
-
} else {
|
|
208
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
209
|
-
fs.copyFileSync(src, dest);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Decide whether a backend entry belongs to the chosen backend.
|
|
215
|
-
* Returns false for entries that must be SKIPPED.
|
|
216
|
-
*/
|
|
217
|
-
function backendEntryKept(basename, isDir, backend) {
|
|
218
|
-
// Never copy runtime artifacts from the template.
|
|
219
|
-
if (isDir && BACKEND_SKIP_DIRS.has(basename)) return false;
|
|
220
|
-
|
|
221
|
-
if (backend === BACKEND.NODE) {
|
|
222
|
-
// Node project: drop every PHP artifact.
|
|
223
|
-
if (basename.endsWith('.php')) return false;
|
|
224
|
-
if (PHP_ONLY_FILES.has(basename)) return false;
|
|
225
|
-
if (isDir && PHP_ONLY_DIRS.has(basename)) return false;
|
|
226
|
-
return true;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// PHP project: drop every Node artifact.
|
|
230
|
-
if (basename.endsWith('.js')) return false;
|
|
231
|
-
if (NODE_ONLY_FILES.has(basename)) return false;
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Copy src/backend into the project keeping only the chosen backend's files.
|
|
237
|
-
* Shared files (SQL migrations, .htaccess, web.config, README, ...) are kept
|
|
238
|
-
* for both. Empty directories left behind by filtering are not created.
|
|
239
|
-
*/
|
|
240
|
-
function copyBackend(src, dest, backend) {
|
|
241
|
-
const stat = fs.statSync(src);
|
|
242
|
-
if (stat.isDirectory()) {
|
|
243
|
-
for (const child of fs.readdirSync(src)) {
|
|
244
|
-
const childSrc = path.join(src, child);
|
|
245
|
-
const isDir = fs.statSync(childSrc).isDirectory();
|
|
246
|
-
if (!backendEntryKept(child, isDir, backend)) continue;
|
|
247
|
-
copyBackend(childSrc, path.join(dest, child), backend);
|
|
248
|
-
}
|
|
249
|
-
} else {
|
|
250
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
251
|
-
fs.copyFileSync(src, dest);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function slashComment(content, line) {
|
|
256
|
-
content = content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
|
|
257
|
-
return content.replace(new RegExp(`^([ \\t]*)(${escapeRegex(line)})$`, 'gm'), '$1// $2');
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function slashUncomment(content, line) {
|
|
261
|
-
return content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function njkComment(content, line) {
|
|
265
|
-
content = content.split(`{# ${line} #}`).join(line);
|
|
266
|
-
return content.split(line).join(`{# ${line} #}`);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function njkUncomment(content, line) {
|
|
270
|
-
return content.split(`{# ${line} #}`).join(line);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function installDependencies(backend) {
|
|
274
|
-
const backendCore = path.join(targetDir, 'src', 'backend', '_core');
|
|
275
|
-
|
|
276
|
-
log(`${color.blue}\n>> Installing Node modules...${color.reset}`);
|
|
277
|
-
const npm = spawnSync('npm', ['install'], {
|
|
278
|
-
cwd: targetDir,
|
|
279
|
-
stdio: 'inherit',
|
|
280
|
-
shell: process.platform === 'win32',
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
if (npm.status !== 0) {
|
|
284
|
-
log('\n(!) npm install failed. Finish manually:');
|
|
285
|
-
if (process.argv[2]) log(` cd ${process.argv[2]}`);
|
|
286
|
-
log(' npm install\n');
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Node backend: its runtime deps were added to the ROOT package.json and
|
|
291
|
-
// installed above into the root node_modules — there is no separate install
|
|
292
|
-
// inside src/backend, and Composer is never run.
|
|
293
|
-
if (backend === BACKEND.NODE) {
|
|
294
|
-
return true;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// --- PHP backend: install Composer dependencies (if present) ---
|
|
298
|
-
if (!fs.existsSync(path.join(backendCore, 'composer.json'))) {
|
|
299
|
-
return true;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const probe = spawnSync('composer', ['--version'], {
|
|
303
|
-
stdio: 'ignore',
|
|
304
|
-
shell: process.platform === 'win32',
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
if (probe.status !== 0) {
|
|
308
|
-
log('\n(!) Composer not found — skipping backend dependencies.');
|
|
309
|
-
log(' Install Composer, then run: cd src/backend/_core && composer install\n');
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
log(`\n${color.blue}>> Installing Composer modules...${color.reset}\n`);
|
|
314
|
-
spawnSync('composer', ['install', '--quiet', '--no-interaction'], {
|
|
315
|
-
cwd: backendCore,
|
|
316
|
-
stdio: 'ignore',
|
|
317
|
-
shell: process.platform === 'win32',
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
return true;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// ── APPLY ─────────────────────────────────────────────────────────────────────
|
|
324
|
-
|
|
325
|
-
function applyFramework(framework) {
|
|
326
|
-
const config = FRAMEWORKS[framework];
|
|
327
|
-
|
|
328
|
-
const globalScssPath = path.join(targetDir, 'src/frontend/scss/modules/_global.scss');
|
|
329
|
-
if (fs.existsSync(globalScssPath)) {
|
|
330
|
-
let content = fs.readFileSync(globalScssPath, 'utf8');
|
|
331
|
-
ALL_FRAMEWORKS.forEach(fw => {
|
|
332
|
-
content = slashComment(content, `@import "../modules/frameworks/${fw}";`);
|
|
333
|
-
});
|
|
334
|
-
if (config.scss) {
|
|
335
|
-
content = slashUncomment(content, `@import "../modules/frameworks/${config.scss}";`);
|
|
336
|
-
}
|
|
337
|
-
fs.writeFileSync(globalScssPath, content);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const baseNjkPath = path.join(targetDir, 'src/frontend/layouts/base.njk');
|
|
341
|
-
if (fs.existsSync(baseNjkPath)) {
|
|
342
|
-
let content = fs.readFileSync(baseNjkPath, 'utf8');
|
|
343
|
-
ALL_FRAMEWORKS.forEach(fw => {
|
|
344
|
-
FRAMEWORKS[fw].njk.forEach(line => { content = njkComment(content, line); });
|
|
345
|
-
});
|
|
346
|
-
config.njk.forEach(line => { content = njkUncomment(content, line); });
|
|
347
|
-
fs.writeFileSync(baseNjkPath, content);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const eleventyPath = path.join(targetDir, '.eleventy.js');
|
|
351
|
-
if (fs.existsSync(eleventyPath)) {
|
|
352
|
-
let content = fs.readFileSync(eleventyPath, 'utf8');
|
|
353
|
-
ALL_FRAMEWORKS.forEach(fw => {
|
|
354
|
-
FRAMEWORKS[fw].eleventy.forEach(line => { content = slashComment(content, line); });
|
|
355
|
-
});
|
|
356
|
-
config.eleventy.forEach(line => { content = slashUncomment(content, line); });
|
|
357
|
-
fs.writeFileSync(eleventyPath, content);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function applyLanguage(language) {
|
|
362
|
-
const eleventyPath = path.join(targetDir, '.eleventy.js');
|
|
363
|
-
if (!fs.existsSync(eleventyPath)) return;
|
|
364
|
-
|
|
365
|
-
let content = fs.readFileSync(eleventyPath, 'utf8');
|
|
366
|
-
|
|
367
|
-
if (language === LANGUAGE.TYPESCRIPT) {
|
|
368
|
-
content = slashComment(content, LANGUAGE_ELEVENTY.jsEntry);
|
|
369
|
-
content = slashUncomment(content, LANGUAGE_ELEVENTY.tsEntry);
|
|
370
|
-
} else {
|
|
371
|
-
content = slashUncomment(content, LANGUAGE_ELEVENTY.jsEntry);
|
|
372
|
-
content = slashComment(content, LANGUAGE_ELEVENTY.tsEntry);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
fs.writeFileSync(eleventyPath, content);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// ── UI ────────────────────────────────────────────────────────────────────────
|
|
379
|
-
|
|
380
|
-
function askChoice(question, choices) {
|
|
381
|
-
return new Promise((resolve) => {
|
|
382
|
-
let selectedIndex = 0;
|
|
383
|
-
|
|
384
|
-
log(`\n>> ${question} (Use arrow keys and press Enter):\n`);
|
|
385
|
-
|
|
386
|
-
const render = (firstTime = false) => {
|
|
387
|
-
if (!firstTime) process.stdout.write(`\x1B[${choices.length}A`);
|
|
388
|
-
const output = choices.map((choice, index) =>
|
|
389
|
-
index === selectedIndex
|
|
390
|
-
? ` \x1b[36m◉ ${choice.label}\x1b[0m\x1B[K\n`
|
|
391
|
-
: ` * ${choice.label}\x1B[K\n`
|
|
392
|
-
).join('');
|
|
393
|
-
process.stdout.write(output);
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
readline.emitKeypressEvents(process.stdin);
|
|
397
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
398
|
-
process.stdin.resume();
|
|
399
|
-
|
|
400
|
-
const onKeyPress = (str, key) => {
|
|
401
|
-
if (key.ctrl && key.name === 'c') {
|
|
402
|
-
process.exit();
|
|
403
|
-
} else if (key.name === 'up') {
|
|
404
|
-
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : choices.length - 1;
|
|
405
|
-
render();
|
|
406
|
-
} else if (key.name === 'down') {
|
|
407
|
-
selectedIndex = selectedIndex < choices.length - 1 ? selectedIndex + 1 : 0;
|
|
408
|
-
render();
|
|
409
|
-
} else if (key.name === 'return' || key.name === 'enter') {
|
|
410
|
-
process.stdin.removeListener('keypress', onKeyPress);
|
|
411
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
412
|
-
process.stdin.pause();
|
|
413
|
-
resolve(choices[selectedIndex].value);
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
process.stdin.on('keypress', onKeyPress);
|
|
418
|
-
render(true);
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// ── INIT ──────────────────────────────────────────────────────────────────────
|
|
423
|
-
|
|
424
|
-
async function init() {
|
|
425
|
-
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
426
|
-
|
|
427
|
-
log(`\n>> ${color.magenta}Creating Nibula project in ${targetDir}\n${color.reset}`);
|
|
428
|
-
|
|
429
|
-
const language = await askChoice('Select a language', LANGUAGE_CHOICES);
|
|
430
|
-
const framework = await askChoice('Select a CSS framework', FRAMEWORK_CHOICES);
|
|
431
|
-
const backend = await askChoice('Select a backend', BACKEND_CHOICES);
|
|
432
|
-
|
|
433
|
-
log('');
|
|
434
|
-
|
|
435
|
-
for (const target of MANDATORY_COPY) {
|
|
436
|
-
const src = path.join(templateDir, target);
|
|
437
|
-
const dest = path.join(targetDir, target);
|
|
438
|
-
if (!fs.existsSync(src)) continue;
|
|
439
|
-
|
|
440
|
-
if (target === 'src/backend') {
|
|
441
|
-
// Copy only the chosen backend's files (the other backend is omitted).
|
|
442
|
-
copyBackend(src, dest, backend);
|
|
443
|
-
} else {
|
|
444
|
-
const exclude = target === 'src/frontend' ? FRONTEND_EXCLUDE[language] : [];
|
|
445
|
-
copyRecursive(src, dest, exclude);
|
|
446
|
-
}
|
|
447
|
-
logAdd(target);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Generate the local config only for the chosen backend. The other backend's
|
|
451
|
-
// example file was not copied, so its guard below is simply skipped.
|
|
452
|
-
const configDest = path.join(targetDir, 'src/backend/config.php');
|
|
453
|
-
const configExample = path.join(targetDir, 'src/backend/example.config.php');
|
|
454
|
-
if (!fs.existsSync(configDest) && fs.existsSync(configExample)) {
|
|
455
|
-
fs.copyFileSync(configExample, configDest);
|
|
456
|
-
logAdd('src/backend/config.php');
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const configJsDest = path.join(targetDir, 'src/backend/config.js');
|
|
460
|
-
const configJsExample = path.join(targetDir, 'src/backend/example.config.js');
|
|
461
|
-
if (!fs.existsSync(configJsDest) && fs.existsSync(configJsExample)) {
|
|
462
|
-
fs.copyFileSync(configJsExample, configJsDest);
|
|
463
|
-
logAdd('src/backend/config.js');
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Build the project package.json. Clone the shared dependency maps so we
|
|
467
|
-
// never mutate the PROJECT_PACKAGE constant.
|
|
468
|
-
const pkg = { ...PROJECT_PACKAGE };
|
|
469
|
-
pkg.dependencies = { ...PROJECT_PACKAGE.dependencies };
|
|
470
|
-
pkg.devDependencies = { ...PROJECT_PACKAGE.devDependencies };
|
|
471
|
-
|
|
472
|
-
// Node backend deps live in the ROOT node_modules — add them to root deps
|
|
473
|
-
// so `npm install` installs them there (never in src/backend).
|
|
474
|
-
if (backend === BACKEND.NODE) {
|
|
475
|
-
pkg.dependencies = { ...pkg.dependencies, ...NODE_BACKEND_DEPENDENCIES };
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (language === LANGUAGE.TYPESCRIPT) {
|
|
479
|
-
const tsSrc = path.join(templateDir, 'tsconfig.json');
|
|
480
|
-
const tsDest = path.join(targetDir, 'tsconfig.json');
|
|
481
|
-
fs.copyFileSync(tsSrc, tsDest);
|
|
482
|
-
logAdd('tsconfig.json');
|
|
483
|
-
pkg.devDependencies = { ...pkg.devDependencies, typescript: 'latest' };
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
487
|
-
logAdd('package.json');
|
|
488
|
-
|
|
489
|
-
fs.writeFileSync(path.join(targetDir, '.gitignore'), GITIGNORE_CONTENT);
|
|
490
|
-
logAdd('.gitignore');
|
|
491
|
-
|
|
492
|
-
for (const dir of CREATE_DIRS) {
|
|
493
|
-
fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
applyFramework(framework);
|
|
497
|
-
applyLanguage(language);
|
|
498
|
-
|
|
499
|
-
installDependencies(backend);
|
|
500
|
-
|
|
501
|
-
log(`\n${color.green}>> Done!${color.reset}`);
|
|
502
|
-
log(`${color.yellow}\nNow run:\n${color.reset}`);
|
|
503
|
-
if (process.argv[2]) log(`${color.yellow}> cd ${process.argv[2]}${color.reset}`);
|
|
504
|
-
log(`${color.yellow}> nib run${color.reset}`);
|
|
505
|
-
log('');
|
|
506
|
-
}
|
|
507
|
-
|
|
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
|
+
const { color } = require('../tools/lib/colors');
|
|
9
|
+
|
|
10
|
+
// ── PATHS ────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const targetDir = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
|
|
13
|
+
const templateDir = path.join(__dirname, '..');
|
|
14
|
+
const SELF_VERSION = require('../package.json').version;
|
|
15
|
+
|
|
16
|
+
// ── ENUMS ────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const LANGUAGE = Object.freeze({
|
|
19
|
+
JAVASCRIPT: 'javascript',
|
|
20
|
+
TYPESCRIPT: 'typescript',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const FRAMEWORK = Object.freeze({
|
|
24
|
+
BOOTSTRAP: 'bootstrap',
|
|
25
|
+
BULMA: 'bulma',
|
|
26
|
+
FOUNDATION: 'foundation',
|
|
27
|
+
UIKIT: 'uikit',
|
|
28
|
+
NONE: 'none',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const BACKEND = Object.freeze({
|
|
32
|
+
NODE: 'node',
|
|
33
|
+
PHP: 'php',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ── CHOICES ──────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const LANGUAGE_CHOICES = [
|
|
39
|
+
{ label: 'JavaScript (recommended)', value: LANGUAGE.JAVASCRIPT },
|
|
40
|
+
{ label: 'TypeScript', value: LANGUAGE.TYPESCRIPT },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const FRAMEWORK_CHOICES = [
|
|
44
|
+
{ label: 'Bootstrap (recommended)', value: FRAMEWORK.BOOTSTRAP },
|
|
45
|
+
{ label: 'Bulma', value: FRAMEWORK.BULMA },
|
|
46
|
+
{ label: 'Foundation', value: FRAMEWORK.FOUNDATION },
|
|
47
|
+
{ label: 'UIkit', value: FRAMEWORK.UIKIT },
|
|
48
|
+
{ label: 'None', value: FRAMEWORK.NONE },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const BACKEND_CHOICES = [
|
|
52
|
+
{ label: 'Node.js (No composer required)', value: BACKEND.NODE },
|
|
53
|
+
{ label: 'PHP (Can run everywhere)', value: BACKEND.PHP },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// Runtime dependency added to the ROOT package.json when the Node backend is
|
|
57
|
+
// chosen, so it lands in the root node_modules (never in src/backend).
|
|
58
|
+
const NODE_BACKEND_DEPENDENCIES = {
|
|
59
|
+
mysql2: '^3.11.0',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ── COPY CONFIG ───────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const MANDATORY_COPY = [
|
|
65
|
+
'.eleventy.js',
|
|
66
|
+
'.eleventyignore',
|
|
67
|
+
'nginx.conf',
|
|
68
|
+
'src/backend',
|
|
69
|
+
'src/frontend',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const FRONTEND_EXCLUDE = {
|
|
73
|
+
[LANGUAGE.JAVASCRIPT]: ['ts'],
|
|
74
|
+
[LANGUAGE.TYPESCRIPT]: ['js'],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const CREATE_DIRS = [
|
|
78
|
+
'src/frontend/_routes',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// Backend files that belong to exactly one backend, matched by basename.
|
|
82
|
+
// Everything else (migrations, .htaccess, web.config, README, ...) is shared.
|
|
83
|
+
const NODE_ONLY_FILES = new Set(['package.json', 'backend-node.service.example']);
|
|
84
|
+
const PHP_ONLY_FILES = new Set(['composer.json', 'composer.lock']);
|
|
85
|
+
const PHP_ONLY_DIRS = new Set(['vendor']);
|
|
86
|
+
// Runtime artifacts that must never be copied from the template, either way.
|
|
87
|
+
const BACKEND_SKIP_DIRS = new Set(['node_modules', 'cache', '.git']);
|
|
88
|
+
|
|
89
|
+
// ── FRAMEWORK CONFIG ──────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
const ALL_FRAMEWORKS = Object.values(FRAMEWORK).filter(f => f !== FRAMEWORK.NONE);
|
|
92
|
+
|
|
93
|
+
const FRAMEWORKS = {
|
|
94
|
+
[FRAMEWORK.BOOTSTRAP]: {
|
|
95
|
+
scss: 'bootstrap',
|
|
96
|
+
njk: ['<script src="/js/bootstrap.bundle.min.js" defer></script>'],
|
|
97
|
+
eleventy: [
|
|
98
|
+
'"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js": "js/bootstrap.bundle.min.js",',
|
|
99
|
+
'"node_modules/bootstrap-icons/font/fonts": "css/fonts",',
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
[FRAMEWORK.BULMA]: {
|
|
103
|
+
scss: 'bulma',
|
|
104
|
+
njk: [],
|
|
105
|
+
eleventy: [],
|
|
106
|
+
},
|
|
107
|
+
[FRAMEWORK.FOUNDATION]: {
|
|
108
|
+
scss: 'foundation',
|
|
109
|
+
njk: ['<script src="/js/foundation.min.js" defer></script>'],
|
|
110
|
+
eleventy: ['"node_modules/foundation-sites/dist/js/foundation.min.js": "js/foundation.min.js",'],
|
|
111
|
+
},
|
|
112
|
+
[FRAMEWORK.UIKIT]: {
|
|
113
|
+
scss: 'uikit',
|
|
114
|
+
njk: [
|
|
115
|
+
'<script src="/js/uikit.min.js" defer></script>',
|
|
116
|
+
'<script src="/js/uikit-icons.min.js" defer></script>',
|
|
117
|
+
],
|
|
118
|
+
eleventy: [
|
|
119
|
+
'"node_modules/uikit/dist/js/uikit.min.js": "js/uikit.min.js",',
|
|
120
|
+
'"node_modules/uikit/dist/js/uikit-icons.min.js": "js/uikit-icons.min.js",',
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
[FRAMEWORK.NONE]: {
|
|
124
|
+
scss: null,
|
|
125
|
+
njk: [],
|
|
126
|
+
eleventy: [],
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// ── LANGUAGE CONFIG ───────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
const LANGUAGE_ELEVENTY = Object.freeze({
|
|
133
|
+
jsEntry: 'const entryPoints = glob.sync("src/frontend/js/pages/*.js");',
|
|
134
|
+
tsEntry: 'const entryPoints = glob.sync("src/frontend/ts/pages/*.ts");',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ── GENERATED FILE CONTENTS ───────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
const GITIGNORE_CONTENT = `
|
|
140
|
+
node_modules/
|
|
141
|
+
src/backend/_core/vendor/
|
|
142
|
+
out/
|
|
143
|
+
src/backend/config.php
|
|
144
|
+
src/backend/config.js
|
|
145
|
+
src/backend/cache/
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
const PROJECT_PACKAGE = {
|
|
150
|
+
name: path.basename(targetDir),
|
|
151
|
+
version: '0.0.0',
|
|
152
|
+
private: true,
|
|
153
|
+
outputDir: 'out',
|
|
154
|
+
"scripts": {
|
|
155
|
+
"build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet --load-path=node_modules",
|
|
156
|
+
"build:js": "nib build-js",
|
|
157
|
+
"build:11ty": "eleventy",
|
|
158
|
+
"build": "npm run clean && npm run build:css && npm run build:js && npm run build:11ty",
|
|
159
|
+
"serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet --load-path=node_modules",
|
|
160
|
+
"serve:js": "nib build-js --watch",
|
|
161
|
+
"serve:11ty": "eleventy --serve --quiet",
|
|
162
|
+
"clean": "nib clean",
|
|
163
|
+
"serve": "npm run clean && concurrently \"npm run serve:11ty\" \"npm run serve:css\" \"npm run serve:js\"",
|
|
164
|
+
"assistant": "nib cli"
|
|
165
|
+
},
|
|
166
|
+
dependencies: {
|
|
167
|
+
'@11ty/eleventy': '^3.1.2',
|
|
168
|
+
'@11ty/eleventy-img': '^6.0.4',
|
|
169
|
+
'bootstrap': '^5.3.8',
|
|
170
|
+
'bootstrap-icons': '^1.13.1',
|
|
171
|
+
'bulma': '^1.0.4',
|
|
172
|
+
'foundation-sites': '^6.9.0',
|
|
173
|
+
'glob': '^13.0.6',
|
|
174
|
+
'uikit': '^3.25.13',
|
|
175
|
+
},
|
|
176
|
+
devDependencies: {
|
|
177
|
+
'nibula': `^${SELF_VERSION}`,
|
|
178
|
+
'concurrently': '^9.2.1',
|
|
179
|
+
'esbuild': '^0.27.3',
|
|
180
|
+
'sass': '^1.77.0',
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// ── HELPERS ───────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
function log(msg) {
|
|
187
|
+
writeSync(1, msg + '\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function logAdd(name) {
|
|
191
|
+
log(`${color.green}+${color.reset} ${name}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function escapeRegex(str) {
|
|
195
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function copyRecursive(src, dest, exclude = []) {
|
|
199
|
+
const stat = fs.statSync(src);
|
|
200
|
+
if (stat.isDirectory()) {
|
|
201
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
202
|
+
for (const child of fs.readdirSync(src)) {
|
|
203
|
+
if (child === '.git') continue;
|
|
204
|
+
if (exclude.includes(child)) continue;
|
|
205
|
+
copyRecursive(path.join(src, child), path.join(dest, child), exclude);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
209
|
+
fs.copyFileSync(src, dest);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Decide whether a backend entry belongs to the chosen backend.
|
|
215
|
+
* Returns false for entries that must be SKIPPED.
|
|
216
|
+
*/
|
|
217
|
+
function backendEntryKept(basename, isDir, backend) {
|
|
218
|
+
// Never copy runtime artifacts from the template.
|
|
219
|
+
if (isDir && BACKEND_SKIP_DIRS.has(basename)) return false;
|
|
220
|
+
|
|
221
|
+
if (backend === BACKEND.NODE) {
|
|
222
|
+
// Node project: drop every PHP artifact.
|
|
223
|
+
if (basename.endsWith('.php')) return false;
|
|
224
|
+
if (PHP_ONLY_FILES.has(basename)) return false;
|
|
225
|
+
if (isDir && PHP_ONLY_DIRS.has(basename)) return false;
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// PHP project: drop every Node artifact.
|
|
230
|
+
if (basename.endsWith('.js')) return false;
|
|
231
|
+
if (NODE_ONLY_FILES.has(basename)) return false;
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Copy src/backend into the project keeping only the chosen backend's files.
|
|
237
|
+
* Shared files (SQL migrations, .htaccess, web.config, README, ...) are kept
|
|
238
|
+
* for both. Empty directories left behind by filtering are not created.
|
|
239
|
+
*/
|
|
240
|
+
function copyBackend(src, dest, backend) {
|
|
241
|
+
const stat = fs.statSync(src);
|
|
242
|
+
if (stat.isDirectory()) {
|
|
243
|
+
for (const child of fs.readdirSync(src)) {
|
|
244
|
+
const childSrc = path.join(src, child);
|
|
245
|
+
const isDir = fs.statSync(childSrc).isDirectory();
|
|
246
|
+
if (!backendEntryKept(child, isDir, backend)) continue;
|
|
247
|
+
copyBackend(childSrc, path.join(dest, child), backend);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
251
|
+
fs.copyFileSync(src, dest);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function slashComment(content, line) {
|
|
256
|
+
content = content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
|
|
257
|
+
return content.replace(new RegExp(`^([ \\t]*)(${escapeRegex(line)})$`, 'gm'), '$1// $2');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function slashUncomment(content, line) {
|
|
261
|
+
return content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function njkComment(content, line) {
|
|
265
|
+
content = content.split(`{# ${line} #}`).join(line);
|
|
266
|
+
return content.split(line).join(`{# ${line} #}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function njkUncomment(content, line) {
|
|
270
|
+
return content.split(`{# ${line} #}`).join(line);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function installDependencies(backend) {
|
|
274
|
+
const backendCore = path.join(targetDir, 'src', 'backend', '_core');
|
|
275
|
+
|
|
276
|
+
log(`${color.blue}\n>> Installing Node modules...${color.reset}`);
|
|
277
|
+
const npm = spawnSync('npm', ['install'], {
|
|
278
|
+
cwd: targetDir,
|
|
279
|
+
stdio: 'inherit',
|
|
280
|
+
shell: process.platform === 'win32',
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (npm.status !== 0) {
|
|
284
|
+
log('\n(!) npm install failed. Finish manually:');
|
|
285
|
+
if (process.argv[2]) log(` cd ${process.argv[2]}`);
|
|
286
|
+
log(' npm install\n');
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Node backend: its runtime deps were added to the ROOT package.json and
|
|
291
|
+
// installed above into the root node_modules — there is no separate install
|
|
292
|
+
// inside src/backend, and Composer is never run.
|
|
293
|
+
if (backend === BACKEND.NODE) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- PHP backend: install Composer dependencies (if present) ---
|
|
298
|
+
if (!fs.existsSync(path.join(backendCore, 'composer.json'))) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const probe = spawnSync('composer', ['--version'], {
|
|
303
|
+
stdio: 'ignore',
|
|
304
|
+
shell: process.platform === 'win32',
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (probe.status !== 0) {
|
|
308
|
+
log('\n(!) Composer not found — skipping backend dependencies.');
|
|
309
|
+
log(' Install Composer, then run: cd src/backend/_core && composer install\n');
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
log(`\n${color.blue}>> Installing Composer modules...${color.reset}\n`);
|
|
314
|
+
spawnSync('composer', ['install', '--quiet', '--no-interaction'], {
|
|
315
|
+
cwd: backendCore,
|
|
316
|
+
stdio: 'ignore',
|
|
317
|
+
shell: process.platform === 'win32',
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── APPLY ─────────────────────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
function applyFramework(framework) {
|
|
326
|
+
const config = FRAMEWORKS[framework];
|
|
327
|
+
|
|
328
|
+
const globalScssPath = path.join(targetDir, 'src/frontend/scss/modules/_global.scss');
|
|
329
|
+
if (fs.existsSync(globalScssPath)) {
|
|
330
|
+
let content = fs.readFileSync(globalScssPath, 'utf8');
|
|
331
|
+
ALL_FRAMEWORKS.forEach(fw => {
|
|
332
|
+
content = slashComment(content, `@import "../modules/frameworks/${fw}";`);
|
|
333
|
+
});
|
|
334
|
+
if (config.scss) {
|
|
335
|
+
content = slashUncomment(content, `@import "../modules/frameworks/${config.scss}";`);
|
|
336
|
+
}
|
|
337
|
+
fs.writeFileSync(globalScssPath, content);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const baseNjkPath = path.join(targetDir, 'src/frontend/layouts/base.njk');
|
|
341
|
+
if (fs.existsSync(baseNjkPath)) {
|
|
342
|
+
let content = fs.readFileSync(baseNjkPath, 'utf8');
|
|
343
|
+
ALL_FRAMEWORKS.forEach(fw => {
|
|
344
|
+
FRAMEWORKS[fw].njk.forEach(line => { content = njkComment(content, line); });
|
|
345
|
+
});
|
|
346
|
+
config.njk.forEach(line => { content = njkUncomment(content, line); });
|
|
347
|
+
fs.writeFileSync(baseNjkPath, content);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const eleventyPath = path.join(targetDir, '.eleventy.js');
|
|
351
|
+
if (fs.existsSync(eleventyPath)) {
|
|
352
|
+
let content = fs.readFileSync(eleventyPath, 'utf8');
|
|
353
|
+
ALL_FRAMEWORKS.forEach(fw => {
|
|
354
|
+
FRAMEWORKS[fw].eleventy.forEach(line => { content = slashComment(content, line); });
|
|
355
|
+
});
|
|
356
|
+
config.eleventy.forEach(line => { content = slashUncomment(content, line); });
|
|
357
|
+
fs.writeFileSync(eleventyPath, content);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function applyLanguage(language) {
|
|
362
|
+
const eleventyPath = path.join(targetDir, '.eleventy.js');
|
|
363
|
+
if (!fs.existsSync(eleventyPath)) return;
|
|
364
|
+
|
|
365
|
+
let content = fs.readFileSync(eleventyPath, 'utf8');
|
|
366
|
+
|
|
367
|
+
if (language === LANGUAGE.TYPESCRIPT) {
|
|
368
|
+
content = slashComment(content, LANGUAGE_ELEVENTY.jsEntry);
|
|
369
|
+
content = slashUncomment(content, LANGUAGE_ELEVENTY.tsEntry);
|
|
370
|
+
} else {
|
|
371
|
+
content = slashUncomment(content, LANGUAGE_ELEVENTY.jsEntry);
|
|
372
|
+
content = slashComment(content, LANGUAGE_ELEVENTY.tsEntry);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fs.writeFileSync(eleventyPath, content);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── UI ────────────────────────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
function askChoice(question, choices) {
|
|
381
|
+
return new Promise((resolve) => {
|
|
382
|
+
let selectedIndex = 0;
|
|
383
|
+
|
|
384
|
+
log(`\n>> ${question} (Use arrow keys and press Enter):\n`);
|
|
385
|
+
|
|
386
|
+
const render = (firstTime = false) => {
|
|
387
|
+
if (!firstTime) process.stdout.write(`\x1B[${choices.length}A`);
|
|
388
|
+
const output = choices.map((choice, index) =>
|
|
389
|
+
index === selectedIndex
|
|
390
|
+
? ` \x1b[36m◉ ${choice.label}\x1b[0m\x1B[K\n`
|
|
391
|
+
: ` * ${choice.label}\x1B[K\n`
|
|
392
|
+
).join('');
|
|
393
|
+
process.stdout.write(output);
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
readline.emitKeypressEvents(process.stdin);
|
|
397
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
398
|
+
process.stdin.resume();
|
|
399
|
+
|
|
400
|
+
const onKeyPress = (str, key) => {
|
|
401
|
+
if (key.ctrl && key.name === 'c') {
|
|
402
|
+
process.exit();
|
|
403
|
+
} else if (key.name === 'up') {
|
|
404
|
+
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : choices.length - 1;
|
|
405
|
+
render();
|
|
406
|
+
} else if (key.name === 'down') {
|
|
407
|
+
selectedIndex = selectedIndex < choices.length - 1 ? selectedIndex + 1 : 0;
|
|
408
|
+
render();
|
|
409
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
410
|
+
process.stdin.removeListener('keypress', onKeyPress);
|
|
411
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
412
|
+
process.stdin.pause();
|
|
413
|
+
resolve(choices[selectedIndex].value);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
process.stdin.on('keypress', onKeyPress);
|
|
418
|
+
render(true);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── INIT ──────────────────────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
async function init() {
|
|
425
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
426
|
+
|
|
427
|
+
log(`\n>> ${color.magenta}Creating Nibula project in ${targetDir}\n${color.reset}`);
|
|
428
|
+
|
|
429
|
+
const language = await askChoice('Select a language', LANGUAGE_CHOICES);
|
|
430
|
+
const framework = await askChoice('Select a CSS framework', FRAMEWORK_CHOICES);
|
|
431
|
+
const backend = await askChoice('Select a backend', BACKEND_CHOICES);
|
|
432
|
+
|
|
433
|
+
log('');
|
|
434
|
+
|
|
435
|
+
for (const target of MANDATORY_COPY) {
|
|
436
|
+
const src = path.join(templateDir, target);
|
|
437
|
+
const dest = path.join(targetDir, target);
|
|
438
|
+
if (!fs.existsSync(src)) continue;
|
|
439
|
+
|
|
440
|
+
if (target === 'src/backend') {
|
|
441
|
+
// Copy only the chosen backend's files (the other backend is omitted).
|
|
442
|
+
copyBackend(src, dest, backend);
|
|
443
|
+
} else {
|
|
444
|
+
const exclude = target === 'src/frontend' ? FRONTEND_EXCLUDE[language] : [];
|
|
445
|
+
copyRecursive(src, dest, exclude);
|
|
446
|
+
}
|
|
447
|
+
logAdd(target);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Generate the local config only for the chosen backend. The other backend's
|
|
451
|
+
// example file was not copied, so its guard below is simply skipped.
|
|
452
|
+
const configDest = path.join(targetDir, 'src/backend/config.php');
|
|
453
|
+
const configExample = path.join(targetDir, 'src/backend/example.config.php');
|
|
454
|
+
if (!fs.existsSync(configDest) && fs.existsSync(configExample)) {
|
|
455
|
+
fs.copyFileSync(configExample, configDest);
|
|
456
|
+
logAdd('src/backend/config.php');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const configJsDest = path.join(targetDir, 'src/backend/config.js');
|
|
460
|
+
const configJsExample = path.join(targetDir, 'src/backend/example.config.js');
|
|
461
|
+
if (!fs.existsSync(configJsDest) && fs.existsSync(configJsExample)) {
|
|
462
|
+
fs.copyFileSync(configJsExample, configJsDest);
|
|
463
|
+
logAdd('src/backend/config.js');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Build the project package.json. Clone the shared dependency maps so we
|
|
467
|
+
// never mutate the PROJECT_PACKAGE constant.
|
|
468
|
+
const pkg = { ...PROJECT_PACKAGE };
|
|
469
|
+
pkg.dependencies = { ...PROJECT_PACKAGE.dependencies };
|
|
470
|
+
pkg.devDependencies = { ...PROJECT_PACKAGE.devDependencies };
|
|
471
|
+
|
|
472
|
+
// Node backend deps live in the ROOT node_modules — add them to root deps
|
|
473
|
+
// so `npm install` installs them there (never in src/backend).
|
|
474
|
+
if (backend === BACKEND.NODE) {
|
|
475
|
+
pkg.dependencies = { ...pkg.dependencies, ...NODE_BACKEND_DEPENDENCIES };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (language === LANGUAGE.TYPESCRIPT) {
|
|
479
|
+
const tsSrc = path.join(templateDir, 'tsconfig.json');
|
|
480
|
+
const tsDest = path.join(targetDir, 'tsconfig.json');
|
|
481
|
+
fs.copyFileSync(tsSrc, tsDest);
|
|
482
|
+
logAdd('tsconfig.json');
|
|
483
|
+
pkg.devDependencies = { ...pkg.devDependencies, typescript: 'latest' };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
487
|
+
logAdd('package.json');
|
|
488
|
+
|
|
489
|
+
fs.writeFileSync(path.join(targetDir, '.gitignore'), GITIGNORE_CONTENT);
|
|
490
|
+
logAdd('.gitignore');
|
|
491
|
+
|
|
492
|
+
for (const dir of CREATE_DIRS) {
|
|
493
|
+
fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
applyFramework(framework);
|
|
497
|
+
applyLanguage(language);
|
|
498
|
+
|
|
499
|
+
installDependencies(backend);
|
|
500
|
+
|
|
501
|
+
log(`\n${color.green}>> Done!${color.reset}`);
|
|
502
|
+
log(`${color.yellow}\nNow run:\n${color.reset}`);
|
|
503
|
+
if (process.argv[2]) log(`${color.yellow}> cd ${process.argv[2]}${color.reset}`);
|
|
504
|
+
log(`${color.yellow}> nib run${color.reset}`);
|
|
505
|
+
log('');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
508
|
init();
|