nibula 1.6.0 → 1.6.2

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