nibula 1.2.3 → 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.
Files changed (106) hide show
  1. package/.eleventy.js +81 -81
  2. package/.eleventyignore +3 -3
  3. package/.github/workflows/publish.yml +37 -0
  4. package/CHANGELOG.md +166 -135
  5. package/LICENSE +169 -169
  6. package/NOTICE +4 -4
  7. package/README.md +120 -123
  8. package/bin/create.js +507 -507
  9. package/bin/nibula.js +316 -307
  10. package/docs/Assistant CLI.md +65 -65
  11. package/docs/Backend.md +295 -295
  12. package/docs/Components.md +84 -84
  13. package/docs/Creating pages.md +64 -64
  14. package/docs/Deploy.md +189 -189
  15. package/docs/Head and SEO.md +138 -138
  16. package/docs/Javascript.md +50 -50
  17. package/docs/Styling with SCSS.md +141 -141
  18. package/nginx.conf +75 -75
  19. package/package.json +1 -1
  20. package/src/backend/.htaccess +6 -6
  21. package/src/backend/_core/composer.json +5 -5
  22. package/src/backend/_core/composer.lock +492 -492
  23. package/src/backend/_core/index.js +267 -267
  24. package/src/backend/_core/index.php +147 -147
  25. package/src/backend/_core/init.js +52 -52
  26. package/src/backend/_core/init.php +33 -33
  27. package/src/backend/_core/modules/RateLimiter.js +58 -58
  28. package/src/backend/_core/modules/RateLimiter.php +30 -30
  29. package/src/backend/_core/modules/Response.js +59 -59
  30. package/src/backend/_core/modules/Response.php +48 -48
  31. package/src/backend/api/protected/example-protected.js +23 -23
  32. package/src/backend/api/protected/example-protected.php +16 -16
  33. package/src/backend/api/public/example-public.js +24 -24
  34. package/src/backend/api/public/example-public.php +16 -16
  35. package/src/backend/backend-node.service.example +30 -30
  36. package/src/backend/database/Database.js +46 -46
  37. package/src/backend/database/Database.php +23 -23
  38. package/src/backend/example.config.js +37 -37
  39. package/src/backend/example.config.php +27 -27
  40. package/src/backend/package.json +18 -18
  41. package/src/backend/web.config +16 -16
  42. package/src/frontend/.htaccess +51 -51
  43. package/src/frontend/404.njk +17 -17
  44. package/src/frontend/assets/brand/favicon.svg +54 -54
  45. package/src/frontend/assets/brand/logo.svg +54 -54
  46. package/src/frontend/components/global/footer.njk +25 -25
  47. package/src/frontend/components/global/header.njk +6 -6
  48. package/src/frontend/components/welcome.njk +114 -114
  49. package/src/frontend/data/site.json +48 -48
  50. package/src/frontend/index.njk +8 -8
  51. package/src/frontend/js/modules/exampleModule.js +2 -2
  52. package/src/frontend/js/pages/404.js +6 -6
  53. package/src/frontend/js/pages/homepage.js +6 -6
  54. package/src/frontend/layouts/base.njk +132 -132
  55. package/src/frontend/layouts/page-components.njk +13 -13
  56. package/src/frontend/llms.njk +17 -17
  57. package/src/frontend/robots.njk +7 -7
  58. package/src/frontend/scss/modules/_animations.scss +24 -24
  59. package/src/frontend/scss/modules/_footer.scss +27 -27
  60. package/src/frontend/scss/modules/_global.scss +47 -47
  61. package/src/frontend/scss/modules/_header.scss +27 -27
  62. package/src/frontend/scss/modules/_mobile.scss +29 -29
  63. package/src/frontend/scss/modules/_root.scss +34 -34
  64. package/src/frontend/scss/modules/_typography.scss +14 -14
  65. package/src/frontend/scss/modules/frameworks/_bootstrap.scss +109 -109
  66. package/src/frontend/scss/modules/frameworks/_bulma.scss +108 -108
  67. package/src/frontend/scss/modules/frameworks/_foundation.scss +138 -139
  68. package/src/frontend/scss/modules/frameworks/_uikit.scss +109 -109
  69. package/src/frontend/scss/pages/404.scss +27 -27
  70. package/src/frontend/scss/pages/homepage.scss +22 -22
  71. package/src/frontend/sitemap.njk +17 -17
  72. package/src/frontend/ts/modules/exampleModule.ts +2 -2
  73. package/src/frontend/ts/pages/404.ts +6 -6
  74. package/src/frontend/ts/pages/homepage.ts +6 -6
  75. package/src/frontend/web.config +55 -55
  76. package/tools/assistant.js +122 -151
  77. package/tools/buildJs.js +39 -37
  78. package/tools/cleanOutput.js +23 -25
  79. package/tools/cli/prompt.js +40 -0
  80. package/tools/cli/ui.js +74 -0
  81. package/tools/config/messages.json +78 -0
  82. package/tools/config/settings.json +135 -0
  83. package/tools/lib/colors.js +17 -0
  84. package/tools/lib/files.js +65 -0
  85. package/tools/lib/logger.js +22 -0
  86. package/tools/lib/outputPath.js +141 -0
  87. package/tools/lib/pageActions.js +138 -0
  88. package/tools/lib/pageArtifacts.js +46 -0
  89. package/tools/lib/pageComponents.js +88 -0
  90. package/tools/lib/paths.js +61 -0
  91. package/tools/lib/project.js +54 -0
  92. package/tools/lib/siteData.js +93 -0
  93. package/tools/lib/text.js +47 -0
  94. package/tools/lib/validation.js +39 -0
  95. package/tools/res/templates/template.js +7 -5
  96. package/tools/res/templates/template.njk +8 -8
  97. package/tools/res/templates/template.scss +22 -22
  98. package/tools/res/templates/template.ts +7 -5
  99. package/tsconfig.json +24 -24
  100. package/tools/modules/constants.js +0 -66
  101. package/tools/modules/pageComponents.js +0 -77
  102. package/tools/modules/updateData.js +0 -90
  103. package/tools/modules/updateOutputPath.js +0 -133
  104. package/tools/modules/updatePage.js +0 -162
  105. package/tools/modules/utils.js +0 -27
  106. package/tools/modules/validation.js +0 -30
@@ -0,0 +1,74 @@
1
+ const settings = require('../config/settings.json');
2
+ const { color } = require('../lib/colors');
3
+ const { message } = require('../lib/logger');
4
+
5
+ const SYMBOLS = settings.cli.symbols;
6
+ const COLORS = settings.cli.colors;
7
+ const MENU = settings.cli.menu;
8
+ const BOX_WIDTH = settings.cli.boxWidth;
9
+ const BOX_TITLE = settings.cli.title;
10
+ const BOX_TOP_LEFT = '\u256d';
11
+ const BOX_TOP_RIGHT = '\u256e';
12
+ const BOX_BOTTOM_LEFT = '\u2570';
13
+ const BOX_BOTTOM_RIGHT = '\u256f';
14
+ const BOX_HORIZONTAL = '\u2500';
15
+ const BOX_VERTICAL = '\u2502';
16
+ const NEW_LINE = '\n';
17
+ const SPACE = ' ';
18
+
19
+ function line(colorName, text) {
20
+ return `${color[colorName]}${text}${color.reset}`;
21
+ }
22
+
23
+ function promptLine(text, prefix = NEW_LINE) {
24
+ return `${prefix}${line(COLORS.prompt, SYMBOLS.prompt)} ${text}`;
25
+ }
26
+
27
+ function error(text) {
28
+ console.log(`${NEW_LINE}${line(COLORS.error, `${SYMBOLS.error} ${text}`)}`);
29
+ }
30
+
31
+ function warning(text) {
32
+ console.log(`${NEW_LINE}${line(COLORS.warning, `${SYMBOLS.warning} ${text}`)}`);
33
+ }
34
+
35
+ function notice(text) {
36
+ console.log(`${NEW_LINE}${line(COLORS.notice, text)}`);
37
+ }
38
+
39
+ function noticePrefix(text) {
40
+ return `${NEW_LINE}${line(COLORS.notice, text)}${NEW_LINE}`;
41
+ }
42
+
43
+ function centeredTitle() {
44
+ const padding = BOX_WIDTH - BOX_TITLE.length;
45
+ const left = SPACE.repeat(Math.ceil(padding / 2));
46
+ const right = SPACE.repeat(Math.floor(padding / 2));
47
+ return `${left}${BOX_TITLE}${right}`;
48
+ }
49
+
50
+ function renderMenu() {
51
+ const border = BOX_HORIZONTAL.repeat(BOX_WIDTH);
52
+
53
+ console.log(`${NEW_LINE}${color[COLORS.box]}${color.bold}${BOX_TOP_LEFT}${border}${BOX_TOP_RIGHT}`);
54
+ console.log(`${BOX_VERTICAL}${centeredTitle()}${BOX_VERTICAL}`);
55
+ console.log(`${BOX_BOTTOM_LEFT}${border}${BOX_BOTTOM_RIGHT}${color.reset}${NEW_LINE}`);
56
+
57
+ for (const item of MENU) {
58
+ console.log(` ${line(COLORS.menuKey, `${item.key}.`)} ${item.label}`);
59
+ }
60
+
61
+ console.log(`${NEW_LINE}${line(COLORS.notice, message('cli.exitHint'))}${NEW_LINE}`);
62
+ }
63
+
64
+ function renderMissingFiles(items) {
65
+ console.log(`${NEW_LINE}${line(COLORS.error, `${SYMBOLS.error} ${message('cli.missingFiles')}`)}`);
66
+
67
+ for (const item of items) {
68
+ console.log(` ${line(COLORS.error, SYMBOLS.bullet)} ${item.label}`);
69
+ }
70
+
71
+ console.log(`${NEW_LINE}${line(COLORS.notice, message('cli.incompleteProject'))}`);
72
+ }
73
+
74
+ module.exports = { promptLine, noticePrefix, error, warning, notice, renderMenu, renderMissingFiles };
@@ -0,0 +1,78 @@
1
+ {
2
+ "project": {
3
+ "notInside": "Not inside a Nibula project.",
4
+ "alreadyInside": "You are already inside a Nibula project.",
5
+ "alreadyInsideHint": "Run \"nib new\" from outside a project."
6
+ },
7
+ "validation": {
8
+ "invalidName": "Invalid name.",
9
+ "invalidNameCharacters": "Page name can only contain lowercase letters, numbers, and hyphens.",
10
+ "nameStartsWithNumber": "Page name cannot start with a number.",
11
+ "protectedName": "\"{name}\" is a protected page name.",
12
+ "invalidPath": "Invalid path.",
13
+ "invalidPathCharacters": "Path contains invalid characters.",
14
+ "templatesLabel": "CLI page templates"
15
+ },
16
+ "page": {
17
+ "alreadyExists": "[skip] page \"{name}\" already exists",
18
+ "doesNotExist": "[skip] page \"{name}\" does not exist",
19
+ "targetExists": "[skip] target page \"{name}\" already exists",
20
+ "templateMissing": "[skip] template not found: {path}",
21
+ "fileMissing": "[skip] not found: {path}",
22
+ "fileCreated": "[created] {path}",
23
+ "fileRenamed": "[renamed] {source} \u2192 {destination}",
24
+ "fileDeleted": "[deleted] {path}",
25
+ "createFailed": "[error] could not create page files: {error}",
26
+ "renameFailed": "[error] could not rename page files: {error}",
27
+ "deleteFailed": "[error] {path}: {error}"
28
+ },
29
+ "components": {
30
+ "fileMissing": "[skip] not found: {path}",
31
+ "anchorMissing": "[skip] no {anchor} anchor in {file}",
32
+ "blockAdded": "[updated] page block added for \"{name}\"",
33
+ "blockMissing": "[skip] page block for \"{name}\" not found",
34
+ "blockRemoved": "[cleaned] page block removed for \"{name}\"",
35
+ "blockRenamed": "[renamed] page block \"{source}\" \u2192 \"{destination}\""
36
+ },
37
+ "siteData": {
38
+ "fileMissing": "[skip] not found: {path}",
39
+ "parseFailed": "[error] cannot parse {file}: {error}",
40
+ "recordExists": "[skip] record \"{name}\" already exists",
41
+ "recordMissing": "[skip] record \"{name}\" not found",
42
+ "recordAdded": "[updated] record \"{name}\" added",
43
+ "recordRemoved": "[cleaned] record \"{name}\" removed",
44
+ "recordRenamed": "[updated] record \"{source}\" renamed to \"{destination}\""
45
+ },
46
+ "output": {
47
+ "emptyPath": "[skip] empty output path",
48
+ "readFailed": "[error] cannot read {file}: {error}",
49
+ "updateFailed": "[error] could not update output path: {error}",
50
+ "updating": "\nupdating output path \u2192 \"{path}\"",
51
+ "variableMissing": "[skip] {variable} not found in {file}",
52
+ "keyMissing": "[skip] {key} not found in {file}",
53
+ "fileUpdated": "[updated] {file} \u2192 {path}",
54
+ "directoryDeleted": "[deleted] {path}",
55
+ "refuseDelete": "[skip] refusing to delete \"{path}\" (outside project)",
56
+ "missingConfiguration": "(!) {key} not found in {file}",
57
+ "cleaned": "(\u2713) cleaned \u2192 {path}",
58
+ "nothingToClean": "(i) nothing to clean \u2192 {path}"
59
+ },
60
+ "cli": {
61
+ "createPrompt": "Name of the new page: ",
62
+ "removePrompt": "Name of the page to remove: ",
63
+ "renameFromPrompt": "Page to rename: ",
64
+ "renameToPrompt": "New name: ",
65
+ "outputPathPrompt": "New output path: ",
66
+ "menuPrompt": "Choose an option: ",
67
+ "currentOutputPath": "Current path: \"{path}\"",
68
+ "confirmRemove": "This permanently deletes all files for \"{name}\".",
69
+ "cancelled": "Cancelled.",
70
+ "pageNotFound": "Page \"{name}\" does not exist.",
71
+ "sameName": "Old and new name are the same.",
72
+ "invalidOption": "Invalid option.",
73
+ "unexpectedError": "Unexpected error: {error}",
74
+ "missingFiles": "This project is missing required files:",
75
+ "incompleteProject": "The project may be incomplete or created with a different Nibula version.",
76
+ "exitHint": "CTRL + C to exit"
77
+ }
78
+ }
@@ -0,0 +1,135 @@
1
+ {
2
+ "project": {
3
+ "marker": ".eleventy.js",
4
+ "defaultOutputDirectory": "out",
5
+ "currentDirectory": ".",
6
+ "outputSuffix": "-out"
7
+ },
8
+ "paths": {
9
+ "eleventyConfig": ".eleventy.js",
10
+ "packageJson": "package.json",
11
+ "typescriptConfig": "tsconfig.json",
12
+ "routes": "src/frontend/routes",
13
+ "stylePages": "src/frontend/scss/pages",
14
+ "siteData": "src/frontend/data/site.json",
15
+ "pageComponents": "src/frontend/layouts/page-components.njk",
16
+ "templates": "tools/res/templates"
17
+ },
18
+ "languages": {
19
+ "javascript": {
20
+ "pages": "src/frontend/js/pages",
21
+ "extension": "js",
22
+ "template": "template.js"
23
+ },
24
+ "typescript": {
25
+ "pages": "src/frontend/ts/pages",
26
+ "extension": "ts",
27
+ "template": "template.ts"
28
+ }
29
+ },
30
+ "packageJson": {
31
+ "outputKey": "outputDir",
32
+ "scriptsKey": "scripts"
33
+ },
34
+ "eleventy": {
35
+ "outputVariable": "OUTPUT_DIR"
36
+ },
37
+ "typescript": {
38
+ "outputKey": "outDir",
39
+ "outputSubdirectory": "ts"
40
+ },
41
+ "build": {
42
+ "scriptOutput": "js/pages"
43
+ },
44
+ "outputScripts": [
45
+ {
46
+ "name": "build:css",
47
+ "marker": "/css",
48
+ "template": "sass src/frontend/scss:{output}/css --no-source-map --style=compressed --quiet --load-path=node_modules"
49
+ },
50
+ {
51
+ "name": "serve:css",
52
+ "marker": "/css",
53
+ "template": "sass --watch src/frontend/scss:{output}/css --no-source-map --quiet --load-path=node_modules"
54
+ },
55
+ {
56
+ "name": "build:js",
57
+ "marker": "/js/pages",
58
+ "template": "esbuild \"{entries}\" --bundle --outdir={output}/js/pages --minify"
59
+ },
60
+ {
61
+ "name": "serve:js",
62
+ "marker": "/js/pages",
63
+ "template": "esbuild \"{entries}\" --bundle --outdir={output}/js/pages --watch"
64
+ }
65
+ ],
66
+ "page": {
67
+ "protected": ["homepage", "404"],
68
+ "styleExtension": "scss",
69
+ "routeExtension": "njk",
70
+ "styleTemplate": "template.scss",
71
+ "routeTemplate": "template.njk",
72
+ "outputFiles": [
73
+ "js/pages/{camelName}.js",
74
+ "css/pages/{camelName}.css",
75
+ "{pageName}.html",
76
+ "pages/{pageName}.html"
77
+ ],
78
+ "outputDirectories": [
79
+ "{pageName}",
80
+ "pages/{pageName}"
81
+ ],
82
+ "dataKeys": {
83
+ "pages": "pages",
84
+ "seo": "seo",
85
+ "title": "title"
86
+ },
87
+ "defaultData": {
88
+ "seo": {
89
+ "title": "{title}",
90
+ "description": "description",
91
+ "keywords": "",
92
+ "noindex": false,
93
+ "canonical": ""
94
+ },
95
+ "cdn": {
96
+ "css": [],
97
+ "js": []
98
+ }
99
+ }
100
+ },
101
+ "pageComponents": {
102
+ "anchor": "{% else %}",
103
+ "condition": "{% elif title == \"{camelName}\" %}",
104
+ "include": "{% include \"component.njk\" ignore missing %}"
105
+ },
106
+ "frontMatter": {
107
+ "title": "title: \"{camelName}\"",
108
+ "permalink": "permalink: \"/{pageName}/\""
109
+ },
110
+ "cli": {
111
+ "title": "Nibula CLI",
112
+ "boxWidth": 17,
113
+ "confirmHint": "[y/N]",
114
+ "symbols": {
115
+ "prompt": "\u276f",
116
+ "error": "\u2716",
117
+ "warning": "\u26a0",
118
+ "bullet": "-"
119
+ },
120
+ "colors": {
121
+ "box": "cyan",
122
+ "menuKey": "dim",
123
+ "prompt": "cyan",
124
+ "error": "red",
125
+ "warning": "yellow",
126
+ "notice": "dim"
127
+ },
128
+ "menu": [
129
+ { "key": "1", "action": "createPage", "label": "Create page" },
130
+ { "key": "2", "action": "removePage", "label": "Remove page" },
131
+ { "key": "3", "action": "renamePage", "label": "Rename page" },
132
+ { "key": "4", "action": "outputPath", "label": "Configure output path" }
133
+ ]
134
+ }
135
+ }
@@ -0,0 +1,17 @@
1
+ const color = Object.freeze({
2
+ reset: '\x1b[0m',
3
+ bold: '\x1b[1m',
4
+ dim: '\x1b[2m',
5
+ red: '\x1b[31m',
6
+ green: '\x1b[32m',
7
+ yellow: '\x1b[33m',
8
+ blue: '\x1b[34m',
9
+ magenta: '\x1b[35m',
10
+ cyan: '\x1b[36m',
11
+ });
12
+
13
+ function paint(name, text) {
14
+ return `${color[name] ?? ''}${text}${color.reset}`;
15
+ }
16
+
17
+ module.exports = { color, paint };
@@ -0,0 +1,65 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const ENCODING = 'utf8';
5
+ const INDENTATION = 2;
6
+ const NEW_LINE = '\n';
7
+
8
+ function exists(target) {
9
+ return fs.existsSync(target);
10
+ }
11
+
12
+ function ensureDirectory(target) {
13
+ fs.mkdirSync(path.dirname(target), { recursive: true });
14
+ }
15
+
16
+ function readText(target) {
17
+ return fs.readFileSync(target, ENCODING);
18
+ }
19
+
20
+ function writeText(target, content) {
21
+ ensureDirectory(target);
22
+ fs.writeFileSync(target, content);
23
+ }
24
+
25
+ function readJson(target) {
26
+ return JSON.parse(readText(target));
27
+ }
28
+
29
+ function writeJson(target, data) {
30
+ writeText(target, `${JSON.stringify(data, null, INDENTATION)}${NEW_LINE}`);
31
+ }
32
+
33
+ function copyFile(source, destination) {
34
+ ensureDirectory(destination);
35
+ fs.copyFileSync(source, destination);
36
+ }
37
+
38
+ function moveFile(source, destination) {
39
+ ensureDirectory(destination);
40
+ fs.renameSync(source, destination);
41
+ }
42
+
43
+ function removeFile(target) {
44
+ if (!exists(target)) return false;
45
+ fs.unlinkSync(target);
46
+ return true;
47
+ }
48
+
49
+ function removeDirectory(target) {
50
+ if (!exists(target)) return false;
51
+ fs.rmSync(target, { recursive: true, force: true });
52
+ return true;
53
+ }
54
+
55
+ module.exports = {
56
+ exists,
57
+ readText,
58
+ writeText,
59
+ readJson,
60
+ writeJson,
61
+ copyFile,
62
+ moveFile,
63
+ removeFile,
64
+ removeDirectory,
65
+ };
@@ -0,0 +1,22 @@
1
+ const messages = require('../config/messages.json');
2
+ const { formatText } = require('./text');
3
+
4
+ const KEY_SEPARATOR = '.';
5
+
6
+ function resolveTemplate(key) {
7
+ return key.split(KEY_SEPARATOR).reduce(
8
+ (current, part) => (current !== null && typeof current === 'object' ? current[part] : undefined),
9
+ messages
10
+ );
11
+ }
12
+
13
+ function message(key, values) {
14
+ const template = resolveTemplate(key);
15
+ return typeof template === 'string' ? formatText(template, values) : key;
16
+ }
17
+
18
+ function log(key, values) {
19
+ console.log(message(key, values));
20
+ }
21
+
22
+ module.exports = { message, log };
@@ -0,0 +1,141 @@
1
+ const path = require('path');
2
+ const settings = require('../config/settings.json');
3
+ const { PATHS } = require('./paths');
4
+ const { readText, writeText, readJson, writeJson, removeDirectory } = require('./files');
5
+ const { log } = require('./logger');
6
+ const { formatText } = require('./text');
7
+ const { isTypeScriptProject, scriptEntries } = require('./project');
8
+
9
+ const OUTPUT_VARIABLE = settings.eleventy.outputVariable;
10
+ const TYPESCRIPT_OUTPUT_KEY = settings.typescript.outputKey;
11
+ const OUTPUT_VARIABLE_PATTERN = new RegExp(`const ${OUTPUT_VARIABLE}\\s*=\\s*['"\`]([^'"\`]*)['"\`]`);
12
+ const TYPESCRIPT_OUTPUT_PATTERN = new RegExp(`"${TYPESCRIPT_OUTPUT_KEY}"\\s*:\\s*"[^"]*"`);
13
+ const WINDOWS_DRIVE_PATTERN = /^[a-zA-Z]:/;
14
+ const BACKSLASH_PATTERN = /\\/g;
15
+ const TRAILING_SLASH_PATTERN = /\/$/;
16
+ const FORWARD_SLASH = '/';
17
+ const RELATIVE_PREFIX = './';
18
+ const EMPTY = '';
19
+
20
+ function parseOutputDirectory(content) {
21
+ const match = content.match(OUTPUT_VARIABLE_PATTERN);
22
+ return match ? match[1] : null;
23
+ }
24
+
25
+ function isAbsolutePath(target) {
26
+ return path.isAbsolute(target) || WINDOWS_DRIVE_PATTERN.test(target);
27
+ }
28
+
29
+ function updateEleventyConfig(newPath) {
30
+ const content = readText(PATHS.eleventyConfig);
31
+ const updated = content.replace(OUTPUT_VARIABLE_PATTERN, `const ${OUTPUT_VARIABLE} = "${newPath}"`);
32
+
33
+ if (content === updated) {
34
+ log('output.variableMissing', { variable: OUTPUT_VARIABLE, file: settings.paths.eleventyConfig });
35
+ return;
36
+ }
37
+ writeText(PATHS.eleventyConfig, updated);
38
+ log('output.fileUpdated', { file: settings.paths.eleventyConfig, path: newPath });
39
+ }
40
+
41
+ function updatePackageJson(newPath, oldPath) {
42
+ const packageJson = readJson(PATHS.packageJson);
43
+ const entries = scriptEntries();
44
+
45
+ packageJson[settings.packageJson.outputKey] = newPath;
46
+ packageJson[settings.packageJson.scriptsKey] = packageJson[settings.packageJson.scriptsKey] || {};
47
+
48
+ const scripts = packageJson[settings.packageJson.scriptsKey];
49
+
50
+ for (const { name, marker, template } of settings.outputScripts) {
51
+ const current = scripts[name];
52
+ const token = oldPath ? `${oldPath}${marker}` : null;
53
+
54
+ if (current && token && current.includes(token)) {
55
+ scripts[name] = current.split(token).join(`${newPath}${marker}`);
56
+ } else {
57
+ scripts[name] = formatText(template, { output: newPath, entries });
58
+ }
59
+ }
60
+
61
+ writeJson(PATHS.packageJson, packageJson);
62
+ log('output.fileUpdated', { file: settings.paths.packageJson, path: newPath });
63
+ }
64
+
65
+ function updateTypeScriptConfig(newPath) {
66
+ if (!isTypeScriptProject()) return;
67
+
68
+ const prefix = isAbsolutePath(newPath) ? EMPTY : RELATIVE_PREFIX;
69
+ const outputDirectory = `${prefix}${newPath}${FORWARD_SLASH}${settings.typescript.outputSubdirectory}`;
70
+ const content = readText(PATHS.typescriptConfig);
71
+ const updated = content.replace(TYPESCRIPT_OUTPUT_PATTERN, `"${TYPESCRIPT_OUTPUT_KEY}": "${outputDirectory}"`);
72
+
73
+ if (content === updated) {
74
+ log('output.keyMissing', { key: TYPESCRIPT_OUTPUT_KEY, file: settings.paths.typescriptConfig });
75
+ return;
76
+ }
77
+ writeText(PATHS.typescriptConfig, updated);
78
+ log('output.fileUpdated', { file: settings.paths.typescriptConfig, path: outputDirectory });
79
+ }
80
+
81
+ function deleteOldOutput(oldPath) {
82
+ if (!oldPath) return;
83
+
84
+ const target = path.resolve(PATHS.root, oldPath);
85
+ const insideProject = target.startsWith(PATHS.root + path.sep);
86
+
87
+ if (!insideProject || target === PATHS.root) {
88
+ log('output.refuseDelete', { path: target });
89
+ return;
90
+ }
91
+ if (removeDirectory(target)) {
92
+ log('output.directoryDeleted', { path: target });
93
+ }
94
+ }
95
+
96
+ function normalizeOutputPath(rawPath) {
97
+ const trimmed = (rawPath ?? '').trim().replace(BACKSLASH_PATTERN, FORWARD_SLASH);
98
+ if (!trimmed) return null;
99
+
100
+ if (trimmed === settings.project.currentDirectory) return settings.project.defaultOutputDirectory;
101
+
102
+ const parent = trimmed.replace(TRAILING_SLASH_PATTERN, EMPTY);
103
+ return `${parent}${FORWARD_SLASH}${path.basename(PATHS.root)}${settings.project.outputSuffix}`;
104
+ }
105
+
106
+ function updateOutputPath(rawPath) {
107
+ const normalizedPath = normalizeOutputPath(rawPath);
108
+ if (!normalizedPath) {
109
+ log('output.emptyPath');
110
+ return;
111
+ }
112
+
113
+ let oldPath = null;
114
+ try {
115
+ oldPath = parseOutputDirectory(readText(PATHS.eleventyConfig));
116
+ } catch (error) {
117
+ log('output.readFailed', { file: settings.paths.eleventyConfig, error: error.message });
118
+ return;
119
+ }
120
+
121
+ deleteOldOutput(oldPath);
122
+ log('output.updating', { path: normalizedPath });
123
+
124
+ try {
125
+ updatePackageJson(normalizedPath, oldPath);
126
+ updateEleventyConfig(normalizedPath);
127
+ updateTypeScriptConfig(normalizedPath);
128
+ } catch (error) {
129
+ log('output.updateFailed', { error: error.message });
130
+ }
131
+ }
132
+
133
+ function getCurrentOutputPath() {
134
+ try {
135
+ return parseOutputDirectory(readText(PATHS.eleventyConfig));
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ module.exports = { updateOutputPath, getCurrentOutputPath };
@@ -0,0 +1,138 @@
1
+ const path = require('path');
2
+ const settings = require('../config/settings.json');
3
+ const { PATHS } = require('./paths');
4
+ const { exists, readText, writeText, copyFile, moveFile, removeFile, removeDirectory } = require('./files');
5
+ const { log } = require('./logger');
6
+ const { formatText } = require('./text');
7
+ const { languageSettings } = require('./project');
8
+ const { getPageArtifacts, pageExists } = require('./pageArtifacts');
9
+ const { addSiteData, removeSiteData, renameSiteData } = require('./siteData');
10
+ const { addPageBlock, removePageBlock, renamePageBlock } = require('./pageComponents');
11
+
12
+ const FRONT_MATTER_PATTERNS = Object.freeze({
13
+ title: /^title:.*$/m,
14
+ permalink: /^permalink:.*$/m,
15
+ });
16
+
17
+ function templatePath(name) {
18
+ return path.join(PATHS.templates, name);
19
+ }
20
+
21
+ function applyFrontMatter(filePath, values) {
22
+ let content = readText(filePath);
23
+
24
+ for (const [key, pattern] of Object.entries(FRONT_MATTER_PATTERNS)) {
25
+ content = content.replace(pattern, formatText(settings.frontMatter[key], values));
26
+ }
27
+
28
+ writeText(filePath, content);
29
+ }
30
+
31
+ function addPage(pageName) {
32
+ if (pageExists(pageName)) {
33
+ log('page.alreadyExists', { name: pageName });
34
+ return;
35
+ }
36
+
37
+ const { camelName, source } = getPageArtifacts(pageName);
38
+ const values = { camelName, pageName };
39
+
40
+ const creations = [
41
+ { destination: source.style, template: settings.page.styleTemplate, isRoute: false },
42
+ { destination: source.script, template: languageSettings().template, isRoute: false },
43
+ { destination: source.route, template: settings.page.routeTemplate, isRoute: true },
44
+ ];
45
+
46
+ try {
47
+ for (const { destination, template, isRoute } of creations) {
48
+ const templateFile = templatePath(template);
49
+
50
+ if (!exists(templateFile)) {
51
+ log('page.templateMissing', { path: templateFile });
52
+ continue;
53
+ }
54
+
55
+ copyFile(templateFile, destination);
56
+ if (isRoute) applyFrontMatter(destination, values);
57
+
58
+ log('page.fileCreated', { path: destination });
59
+ }
60
+ } catch (error) {
61
+ log('page.createFailed', { error: error.message });
62
+ return;
63
+ }
64
+
65
+ addPageBlock(pageName);
66
+ addSiteData(pageName);
67
+ }
68
+
69
+ function renamePage(oldName, newName) {
70
+ if (!pageExists(oldName)) {
71
+ log('page.doesNotExist', { name: oldName });
72
+ return;
73
+ }
74
+ if (pageExists(newName)) {
75
+ log('page.targetExists', { name: newName });
76
+ return;
77
+ }
78
+
79
+ const current = getPageArtifacts(oldName);
80
+ const next = getPageArtifacts(newName);
81
+ const values = { camelName: next.camelName, pageName: newName };
82
+
83
+ const moves = [
84
+ { source: current.source.style, destination: next.source.style, isRoute: false },
85
+ { source: current.source.script, destination: next.source.script, isRoute: false },
86
+ { source: current.source.route, destination: next.source.route, isRoute: true },
87
+ ];
88
+
89
+ try {
90
+ for (const { source, destination, isRoute } of moves) {
91
+ if (!exists(source)) {
92
+ log('page.fileMissing', { path: source });
93
+ continue;
94
+ }
95
+
96
+ moveFile(source, destination);
97
+ log('page.fileRenamed', { source, destination });
98
+
99
+ if (isRoute) applyFrontMatter(destination, values);
100
+ }
101
+ } catch (error) {
102
+ log('page.renameFailed', { error: error.message });
103
+ return;
104
+ }
105
+
106
+ renamePageBlock(oldName, newName);
107
+ renameSiteData(oldName, newName);
108
+ }
109
+
110
+ function removePage(pageName) {
111
+ if (!pageExists(pageName)) {
112
+ log('page.doesNotExist', { name: pageName });
113
+ }
114
+
115
+ const { source, output } = getPageArtifacts(pageName);
116
+ const files = [source.style, source.script, source.route, ...output.files];
117
+
118
+ for (const file of files) {
119
+ try {
120
+ if (removeFile(file)) log('page.fileDeleted', { path: file });
121
+ } catch (error) {
122
+ log('page.deleteFailed', { path: file, error: error.message });
123
+ }
124
+ }
125
+
126
+ for (const directory of output.directories) {
127
+ try {
128
+ if (removeDirectory(directory)) log('page.fileDeleted', { path: directory });
129
+ } catch (error) {
130
+ log('page.deleteFailed', { path: directory, error: error.message });
131
+ }
132
+ }
133
+
134
+ removePageBlock(pageName);
135
+ removeSiteData(pageName);
136
+ }
137
+
138
+ module.exports = { addPage, removePage, renamePage };