create-forgeon 0.1.2 → 0.1.6

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 (143) hide show
  1. package/README.md +19 -17
  2. package/bin/create-forgeon.mjs +22 -22
  3. package/package.json +1 -1
  4. package/src/cli/add-help.mjs +12 -12
  5. package/src/cli/add-options.mjs +54 -54
  6. package/src/cli/add-options.test.mjs +24 -24
  7. package/src/cli/help.mjs +20 -20
  8. package/src/cli/options.mjs +121 -121
  9. package/src/cli/options.test.mjs +41 -41
  10. package/src/cli/prompt-select.mjs +94 -94
  11. package/src/cli/prompt-select.test.mjs +148 -148
  12. package/src/constants.mjs +13 -13
  13. package/src/core/docs.mjs +128 -128
  14. package/src/core/docs.test.mjs +91 -91
  15. package/src/core/install.mjs +14 -14
  16. package/src/core/scaffold.mjs +48 -45
  17. package/src/core/validate.mjs +12 -12
  18. package/src/core/validate.test.mjs +73 -73
  19. package/src/databases/index.mjs +26 -26
  20. package/src/frameworks/index.mjs +32 -32
  21. package/src/infrastructure/proxy.mjs +12 -12
  22. package/src/modules/docs.mjs +70 -70
  23. package/src/modules/executor.mjs +39 -21
  24. package/src/modules/executor.test.mjs +95 -45
  25. package/src/modules/i18n.mjs +283 -0
  26. package/src/modules/registry.mjs +43 -35
  27. package/src/presets/i18n.mjs +228 -180
  28. package/src/presets/index.mjs +2 -2
  29. package/src/presets/proxy.mjs +32 -32
  30. package/src/run-add-module.mjs +47 -47
  31. package/src/run-create-forgeon.mjs +72 -72
  32. package/src/utils/fs.mjs +26 -26
  33. package/src/utils/values.mjs +20 -20
  34. package/templates/base/.dockerignore +7 -7
  35. package/templates/base/.editorconfig +11 -11
  36. package/templates/base/README.md +46 -46
  37. package/templates/base/apps/api/Dockerfile +24 -24
  38. package/templates/base/apps/api/package.json +39 -39
  39. package/templates/base/apps/api/prisma/migrations/0001_init/migration.sql +11 -11
  40. package/templates/base/apps/api/prisma/schema.prisma +14 -14
  41. package/templates/base/apps/api/prisma/seed.ts +19 -19
  42. package/templates/base/apps/api/src/app.module.ts +32 -32
  43. package/templates/base/apps/api/src/common/dto/echo-query.dto.ts +5 -5
  44. package/templates/base/apps/api/src/common/filters/app-exception.filter.ts +129 -129
  45. package/templates/base/apps/api/src/config/app.config.ts +12 -12
  46. package/templates/base/apps/api/src/health/health.controller.ts +30 -30
  47. package/templates/base/apps/api/src/main.ts +25 -25
  48. package/templates/base/apps/api/src/prisma/prisma.module.ts +8 -8
  49. package/templates/base/apps/api/src/prisma/prisma.service.ts +26 -26
  50. package/templates/base/apps/api/tsconfig.build.json +8 -8
  51. package/templates/base/apps/api/tsconfig.json +8 -8
  52. package/templates/base/apps/web/Dockerfile +12 -12
  53. package/templates/base/apps/web/index.html +11 -11
  54. package/templates/base/apps/web/package.json +21 -21
  55. package/templates/base/apps/web/src/App.tsx +35 -35
  56. package/templates/base/apps/web/src/main.tsx +8 -8
  57. package/templates/base/apps/web/src/styles.css +32 -32
  58. package/templates/base/apps/web/tsconfig.json +17 -17
  59. package/templates/base/apps/web/vite.config.ts +14 -14
  60. package/templates/base/docs/AI/ARCHITECTURE.md +37 -37
  61. package/templates/base/docs/AI/MODULE_SPEC.md +56 -56
  62. package/templates/base/docs/AI/PROJECT.md +31 -31
  63. package/templates/base/docs/AI/TASKS.md +57 -57
  64. package/templates/base/docs/README.md +6 -6
  65. package/templates/base/infra/caddy/Caddyfile +15 -15
  66. package/templates/base/infra/docker/.env.example +9 -9
  67. package/templates/base/infra/docker/caddy.Dockerfile +15 -15
  68. package/templates/base/infra/docker/compose.caddy.yml +44 -44
  69. package/templates/base/infra/docker/compose.nginx.yml +44 -44
  70. package/templates/base/infra/docker/compose.none.yml +37 -37
  71. package/templates/base/infra/docker/compose.yml +44 -44
  72. package/templates/base/infra/docker/nginx.Dockerfile +15 -15
  73. package/templates/base/infra/nginx/nginx.conf +31 -31
  74. package/templates/base/package.json +23 -23
  75. package/templates/base/packages/core/README.md +2 -2
  76. package/templates/base/packages/core/package.json +13 -13
  77. package/templates/base/packages/core/tsconfig.json +7 -7
  78. package/templates/base/packages/i18n/package.json +18 -18
  79. package/templates/base/packages/i18n/src/forgeon-i18n.module.ts +45 -45
  80. package/templates/base/packages/i18n/tsconfig.json +8 -8
  81. package/templates/base/pnpm-workspace.yaml +2 -2
  82. package/templates/base/resources/i18n/en/common.json +4 -4
  83. package/templates/base/resources/i18n/en/validation.json +2 -2
  84. package/templates/base/resources/i18n/uk/common.json +4 -4
  85. package/templates/base/resources/i18n/uk/validation.json +2 -2
  86. package/templates/base/tsconfig.base.json +16 -16
  87. package/templates/docs-fragments/AI_ARCHITECTURE/00_title.md +1 -1
  88. package/templates/docs-fragments/AI_ARCHITECTURE/10_layout_base.md +6 -6
  89. package/templates/docs-fragments/AI_ARCHITECTURE/11_layout_infra.md +1 -1
  90. package/templates/docs-fragments/AI_ARCHITECTURE/12_layout_i18n_resources.md +1 -1
  91. package/templates/docs-fragments/AI_ARCHITECTURE/20_env_base.md +4 -4
  92. package/templates/docs-fragments/AI_ARCHITECTURE/21_env_i18n.md +3 -3
  93. package/templates/docs-fragments/AI_ARCHITECTURE/30_default_db.md +7 -7
  94. package/templates/docs-fragments/AI_ARCHITECTURE/31_docker_runtime.md +5 -5
  95. package/templates/docs-fragments/AI_ARCHITECTURE/32_scope_freeze.md +5 -5
  96. package/templates/docs-fragments/AI_ARCHITECTURE/40_docs_generation.md +9 -9
  97. package/templates/docs-fragments/AI_ARCHITECTURE/50_extension_points.md +8 -8
  98. package/templates/docs-fragments/AI_PROJECT/00_title.md +1 -1
  99. package/templates/docs-fragments/AI_PROJECT/10_what_is.md +3 -3
  100. package/templates/docs-fragments/AI_PROJECT/20_structure_base.md +5 -5
  101. package/templates/docs-fragments/AI_PROJECT/21_structure_i18n.md +2 -0
  102. package/templates/docs-fragments/AI_PROJECT/22_structure_docker.md +1 -1
  103. package/templates/docs-fragments/AI_PROJECT/23_structure_docs.md +1 -1
  104. package/templates/docs-fragments/AI_PROJECT/30_run_dev.md +8 -8
  105. package/templates/docs-fragments/AI_PROJECT/31_run_docker.md +5 -5
  106. package/templates/docs-fragments/AI_PROJECT/32_proxy_notes.md +5 -5
  107. package/templates/docs-fragments/AI_PROJECT/32_proxy_notes_none.md +5 -5
  108. package/templates/docs-fragments/AI_PROJECT/33_i18n_notes.md +2 -0
  109. package/templates/docs-fragments/AI_PROJECT/40_change_boundaries_base.md +3 -3
  110. package/templates/docs-fragments/AI_PROJECT/41_change_boundaries_docker.md +1 -1
  111. package/templates/docs-fragments/README/00_title.md +3 -3
  112. package/templates/docs-fragments/README/10_stack.md +8 -8
  113. package/templates/docs-fragments/README/20_quick_start_dev_intro.md +6 -6
  114. package/templates/docs-fragments/README/21_quick_start_dev_db_docker.md +4 -4
  115. package/templates/docs-fragments/README/21_quick_start_dev_db_local.md +1 -1
  116. package/templates/docs-fragments/README/22_quick_start_dev_outro.md +7 -7
  117. package/templates/docs-fragments/README/30_quick_start_docker.md +7 -7
  118. package/templates/docs-fragments/README/30_quick_start_docker_none.md +9 -9
  119. package/templates/docs-fragments/README/31_proxy_preset_caddy.md +9 -9
  120. package/templates/docs-fragments/README/31_proxy_preset_nginx.md +8 -8
  121. package/templates/docs-fragments/README/31_proxy_preset_none.md +6 -6
  122. package/templates/docs-fragments/README/32_prisma_container_start.md +5 -5
  123. package/templates/docs-fragments/README/40_i18n.md +14 -8
  124. package/templates/docs-fragments/README/90_next_steps.md +7 -7
  125. package/templates/module-fragments/i18n/00_title.md +5 -0
  126. package/templates/module-fragments/i18n/10_overview.md +9 -0
  127. package/templates/module-fragments/i18n/20_scope.md +7 -0
  128. package/templates/module-fragments/i18n/90_status_implemented.md +3 -0
  129. package/templates/module-fragments/jwt-auth/00_title.md +1 -1
  130. package/templates/module-fragments/jwt-auth/10_overview.md +6 -6
  131. package/templates/module-fragments/jwt-auth/20_scope.md +7 -7
  132. package/templates/module-fragments/jwt-auth/90_status_planned.md +3 -3
  133. package/templates/module-fragments/queue/00_title.md +1 -1
  134. package/templates/module-fragments/queue/10_overview.md +6 -6
  135. package/templates/module-fragments/queue/20_scope.md +7 -7
  136. package/templates/module-fragments/queue/90_status_planned.md +3 -3
  137. package/templates/module-presets/i18n/apps/web/src/App.tsx +61 -0
  138. package/templates/module-presets/i18n/packages/i18n-contracts/package.json +14 -0
  139. package/templates/module-presets/i18n/packages/i18n-contracts/src/index.ts +7 -0
  140. package/templates/module-presets/i18n/packages/i18n-contracts/tsconfig.json +8 -0
  141. package/templates/module-presets/i18n/packages/i18n-web/package.json +17 -0
  142. package/templates/module-presets/i18n/packages/i18n-web/src/index.ts +50 -0
  143. package/templates/module-presets/i18n/packages/i18n-web/tsconfig.json +8 -0
package/src/core/docs.mjs CHANGED
@@ -1,128 +1,128 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { getDatabaseLabel } from '../databases/index.mjs';
4
- import { getFrontendLabel } from '../frameworks/index.mjs';
5
- import { getProxyConfigPath } from '../infrastructure/proxy.mjs';
6
-
7
- function renderTemplate(content, variables) {
8
- return content.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => String(variables[key] ?? ''));
9
- }
10
-
11
- function readFragment(fragmentsRoot, docKey, fragmentName, variables) {
12
- const fragmentPath = path.join(fragmentsRoot, docKey, `${fragmentName}.md`);
13
- if (!fs.existsSync(fragmentPath)) {
14
- throw new Error(`Missing docs fragment: ${fragmentPath}`);
15
- }
16
-
17
- const raw = fs.readFileSync(fragmentPath, 'utf8').replace(/\r\n/g, '\n').trim();
18
- return renderTemplate(raw, variables).trim();
19
- }
20
-
21
- function writeDocFromFragments({
22
- targetRoot,
23
- outputPath,
24
- fragmentsRoot,
25
- docKey,
26
- fragmentNames,
27
- variables,
28
- }) {
29
- const fragments = fragmentNames
30
- .map((fragmentName) => readFragment(fragmentsRoot, docKey, fragmentName, variables))
31
- .filter((fragment) => fragment.length > 0);
32
-
33
- const content = `${fragments
34
- .join('\n\n')
35
- .replace(/\n{2,}(?=- )/g, '\n')
36
- .replace(/\n{3,}/g, '\n\n')
37
- .trimEnd()}\n`;
38
- const absoluteOutputPath = path.join(targetRoot, outputPath);
39
- fs.mkdirSync(path.dirname(absoluteOutputPath), { recursive: true });
40
- fs.writeFileSync(absoluteOutputPath, content, 'utf8');
41
- }
42
-
43
- export function generateDocs(targetRoot, options, packageRoot) {
44
- const fragmentsRoot = path.resolve(packageRoot, 'templates', 'docs-fragments');
45
- const variables = {
46
- FRONTEND_LABEL: getFrontendLabel(options.frontend),
47
- DB_LABEL: getDatabaseLabel(options.db),
48
- I18N_STATUS: options.i18nEnabled ? 'enabled' : 'disabled',
49
- DOCKER_STATUS: 'enabled',
50
- PROXY_LABEL: options.proxy,
51
- PROXY_CONFIG_PATH: getProxyConfigPath(options.proxy),
52
- };
53
-
54
- const readmeFragments = ['00_title', '10_stack', '20_quick_start_dev_intro'];
55
- readmeFragments.push('21_quick_start_dev_db_docker');
56
- readmeFragments.push('22_quick_start_dev_outro');
57
- readmeFragments.push(
58
- options.proxy === 'none' ? '30_quick_start_docker_none' : '30_quick_start_docker',
59
- );
60
- if (options.proxy === 'caddy') {
61
- readmeFragments.push('31_proxy_preset_caddy');
62
- } else if (options.proxy === 'nginx') {
63
- readmeFragments.push('31_proxy_preset_nginx');
64
- } else {
65
- readmeFragments.push('31_proxy_preset_none');
66
- }
67
- readmeFragments.push('32_prisma_container_start');
68
- if (options.i18nEnabled) {
69
- readmeFragments.push('40_i18n');
70
- }
71
- readmeFragments.push('90_next_steps');
72
-
73
- const aiProjectFragments = ['00_title', '10_what_is', '20_structure_base'];
74
- if (options.i18nEnabled) {
75
- aiProjectFragments.push('21_structure_i18n');
76
- }
77
- aiProjectFragments.push('22_structure_docker', '23_structure_docs', '30_run_dev', '31_run_docker');
78
- if (options.proxy === 'none') {
79
- aiProjectFragments.push('32_proxy_notes_none');
80
- } else {
81
- aiProjectFragments.push('32_proxy_notes');
82
- }
83
- if (options.i18nEnabled) {
84
- aiProjectFragments.push('33_i18n_notes');
85
- }
86
- aiProjectFragments.push('40_change_boundaries_base');
87
- if (options.proxy !== 'none') {
88
- aiProjectFragments.push('41_change_boundaries_docker');
89
- }
90
-
91
- const aiArchitectureFragments = ['00_title', '10_layout_base', '11_layout_infra'];
92
- if (options.i18nEnabled) {
93
- aiArchitectureFragments.push('12_layout_i18n_resources');
94
- }
95
- aiArchitectureFragments.push('20_env_base');
96
- if (options.i18nEnabled) {
97
- aiArchitectureFragments.push('21_env_i18n');
98
- }
99
- aiArchitectureFragments.push('30_default_db', '31_docker_runtime', '32_scope_freeze');
100
- aiArchitectureFragments.push('40_docs_generation', '50_extension_points');
101
-
102
- writeDocFromFragments({
103
- targetRoot,
104
- outputPath: 'README.md',
105
- fragmentsRoot,
106
- docKey: 'README',
107
- fragmentNames: readmeFragments,
108
- variables,
109
- });
110
-
111
- writeDocFromFragments({
112
- targetRoot,
113
- outputPath: path.join('docs', 'AI', 'PROJECT.md'),
114
- fragmentsRoot,
115
- docKey: 'AI_PROJECT',
116
- fragmentNames: aiProjectFragments,
117
- variables,
118
- });
119
-
120
- writeDocFromFragments({
121
- targetRoot,
122
- outputPath: path.join('docs', 'AI', 'ARCHITECTURE.md'),
123
- fragmentsRoot,
124
- docKey: 'AI_ARCHITECTURE',
125
- fragmentNames: aiArchitectureFragments,
126
- variables,
127
- });
128
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getDatabaseLabel } from '../databases/index.mjs';
4
+ import { getFrontendLabel } from '../frameworks/index.mjs';
5
+ import { getProxyConfigPath } from '../infrastructure/proxy.mjs';
6
+
7
+ function renderTemplate(content, variables) {
8
+ return content.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => String(variables[key] ?? ''));
9
+ }
10
+
11
+ function readFragment(fragmentsRoot, docKey, fragmentName, variables) {
12
+ const fragmentPath = path.join(fragmentsRoot, docKey, `${fragmentName}.md`);
13
+ if (!fs.existsSync(fragmentPath)) {
14
+ throw new Error(`Missing docs fragment: ${fragmentPath}`);
15
+ }
16
+
17
+ const raw = fs.readFileSync(fragmentPath, 'utf8').replace(/\r\n/g, '\n').trim();
18
+ return renderTemplate(raw, variables).trim();
19
+ }
20
+
21
+ function writeDocFromFragments({
22
+ targetRoot,
23
+ outputPath,
24
+ fragmentsRoot,
25
+ docKey,
26
+ fragmentNames,
27
+ variables,
28
+ }) {
29
+ const fragments = fragmentNames
30
+ .map((fragmentName) => readFragment(fragmentsRoot, docKey, fragmentName, variables))
31
+ .filter((fragment) => fragment.length > 0);
32
+
33
+ const content = `${fragments
34
+ .join('\n\n')
35
+ .replace(/\n{2,}(?=- )/g, '\n')
36
+ .replace(/\n{3,}/g, '\n\n')
37
+ .trimEnd()}\n`;
38
+ const absoluteOutputPath = path.join(targetRoot, outputPath);
39
+ fs.mkdirSync(path.dirname(absoluteOutputPath), { recursive: true });
40
+ fs.writeFileSync(absoluteOutputPath, content, 'utf8');
41
+ }
42
+
43
+ export function generateDocs(targetRoot, options, packageRoot) {
44
+ const fragmentsRoot = path.resolve(packageRoot, 'templates', 'docs-fragments');
45
+ const variables = {
46
+ FRONTEND_LABEL: getFrontendLabel(options.frontend),
47
+ DB_LABEL: getDatabaseLabel(options.db),
48
+ I18N_STATUS: options.i18nEnabled ? 'enabled' : 'disabled',
49
+ DOCKER_STATUS: 'enabled',
50
+ PROXY_LABEL: options.proxy,
51
+ PROXY_CONFIG_PATH: getProxyConfigPath(options.proxy),
52
+ };
53
+
54
+ const readmeFragments = ['00_title', '10_stack', '20_quick_start_dev_intro'];
55
+ readmeFragments.push('21_quick_start_dev_db_docker');
56
+ readmeFragments.push('22_quick_start_dev_outro');
57
+ readmeFragments.push(
58
+ options.proxy === 'none' ? '30_quick_start_docker_none' : '30_quick_start_docker',
59
+ );
60
+ if (options.proxy === 'caddy') {
61
+ readmeFragments.push('31_proxy_preset_caddy');
62
+ } else if (options.proxy === 'nginx') {
63
+ readmeFragments.push('31_proxy_preset_nginx');
64
+ } else {
65
+ readmeFragments.push('31_proxy_preset_none');
66
+ }
67
+ readmeFragments.push('32_prisma_container_start');
68
+ if (options.i18nEnabled) {
69
+ readmeFragments.push('40_i18n');
70
+ }
71
+ readmeFragments.push('90_next_steps');
72
+
73
+ const aiProjectFragments = ['00_title', '10_what_is', '20_structure_base'];
74
+ if (options.i18nEnabled) {
75
+ aiProjectFragments.push('21_structure_i18n');
76
+ }
77
+ aiProjectFragments.push('22_structure_docker', '23_structure_docs', '30_run_dev', '31_run_docker');
78
+ if (options.proxy === 'none') {
79
+ aiProjectFragments.push('32_proxy_notes_none');
80
+ } else {
81
+ aiProjectFragments.push('32_proxy_notes');
82
+ }
83
+ if (options.i18nEnabled) {
84
+ aiProjectFragments.push('33_i18n_notes');
85
+ }
86
+ aiProjectFragments.push('40_change_boundaries_base');
87
+ if (options.proxy !== 'none') {
88
+ aiProjectFragments.push('41_change_boundaries_docker');
89
+ }
90
+
91
+ const aiArchitectureFragments = ['00_title', '10_layout_base', '11_layout_infra'];
92
+ if (options.i18nEnabled) {
93
+ aiArchitectureFragments.push('12_layout_i18n_resources');
94
+ }
95
+ aiArchitectureFragments.push('20_env_base');
96
+ if (options.i18nEnabled) {
97
+ aiArchitectureFragments.push('21_env_i18n');
98
+ }
99
+ aiArchitectureFragments.push('30_default_db', '31_docker_runtime', '32_scope_freeze');
100
+ aiArchitectureFragments.push('40_docs_generation', '50_extension_points');
101
+
102
+ writeDocFromFragments({
103
+ targetRoot,
104
+ outputPath: 'README.md',
105
+ fragmentsRoot,
106
+ docKey: 'README',
107
+ fragmentNames: readmeFragments,
108
+ variables,
109
+ });
110
+
111
+ writeDocFromFragments({
112
+ targetRoot,
113
+ outputPath: path.join('docs', 'AI', 'PROJECT.md'),
114
+ fragmentsRoot,
115
+ docKey: 'AI_PROJECT',
116
+ fragmentNames: aiProjectFragments,
117
+ variables,
118
+ });
119
+
120
+ writeDocFromFragments({
121
+ targetRoot,
122
+ outputPath: path.join('docs', 'AI', 'ARCHITECTURE.md'),
123
+ fragmentsRoot,
124
+ docKey: 'AI_ARCHITECTURE',
125
+ fragmentNames: aiArchitectureFragments,
126
+ variables,
127
+ });
128
+ }
@@ -1,91 +1,91 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import { generateDocs } from './docs.mjs';
8
-
9
- function makeTempDir(prefix) {
10
- return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
11
- }
12
-
13
- function readFile(filePath) {
14
- return fs.readFileSync(filePath, 'utf8');
15
- }
16
-
17
- describe('generateDocs', () => {
18
- const thisDir = path.dirname(fileURLToPath(import.meta.url));
19
- const packageRoot = path.resolve(thisDir, '..', '..');
20
-
21
- it('generates docs for proxy=none without i18n section', () => {
22
- const targetRoot = makeTempDir('forgeon-docs-off-');
23
-
24
- try {
25
- generateDocs(
26
- targetRoot,
27
- {
28
- frontend: 'react',
29
- db: 'prisma',
30
- dockerEnabled: true,
31
- i18nEnabled: false,
32
- proxy: 'none',
33
- },
34
- packageRoot,
35
- );
36
-
37
- const readme = readFile(path.join(targetRoot, 'README.md'));
38
- const projectDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'PROJECT.md'));
39
- const architectureDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'ARCHITECTURE.md'));
40
-
41
- assert.match(readme, /Docker\/infra: `enabled`/);
42
- assert.match(readme, /Quick Start \(Docker\)/);
43
- assert.match(readme, /Proxy Preset: none/);
44
- assert.doesNotMatch(readme, /i18n Configuration/);
45
-
46
- assert.match(projectDoc, /### Docker mode/);
47
- assert.match(projectDoc, /Active proxy preset: `none`/);
48
- assert.doesNotMatch(projectDoc, /packages\/i18n/);
49
-
50
- assert.match(architectureDoc, /infra\/\*/);
51
- assert.doesNotMatch(architectureDoc, /I18N_ENABLED/);
52
- } finally {
53
- fs.rmSync(targetRoot, { recursive: true, force: true });
54
- }
55
- });
56
-
57
- it('generates docker and caddy notes when enabled', () => {
58
- const targetRoot = makeTempDir('forgeon-docs-on-');
59
-
60
- try {
61
- generateDocs(
62
- targetRoot,
63
- {
64
- frontend: 'react',
65
- db: 'prisma',
66
- dockerEnabled: true,
67
- i18nEnabled: true,
68
- proxy: 'caddy',
69
- },
70
- packageRoot,
71
- );
72
-
73
- const readme = readFile(path.join(targetRoot, 'README.md'));
74
- const projectDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'PROJECT.md'));
75
- const architectureDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'ARCHITECTURE.md'));
76
-
77
- assert.match(readme, /Quick Start \(Docker\)/);
78
- assert.match(readme, /Proxy Preset: Caddy/);
79
- assert.match(readme, /i18n Configuration/);
80
-
81
- assert.match(projectDoc, /`infra` - Docker Compose \(always\) \+ proxy preset \(`caddy`\)/);
82
- assert.match(projectDoc, /Main proxy config: `infra\/caddy\/Caddyfile`/);
83
-
84
- assert.match(architectureDoc, /infra\/\*/);
85
- assert.match(architectureDoc, /I18N_ENABLED/);
86
- assert.match(architectureDoc, /Active reverse proxy preset: `caddy`/);
87
- } finally {
88
- fs.rmSync(targetRoot, { recursive: true, force: true });
89
- }
90
- });
91
- });
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { generateDocs } from './docs.mjs';
8
+
9
+ function makeTempDir(prefix) {
10
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
11
+ }
12
+
13
+ function readFile(filePath) {
14
+ return fs.readFileSync(filePath, 'utf8');
15
+ }
16
+
17
+ describe('generateDocs', () => {
18
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
19
+ const packageRoot = path.resolve(thisDir, '..', '..');
20
+
21
+ it('generates docs for proxy=none without i18n section', () => {
22
+ const targetRoot = makeTempDir('forgeon-docs-off-');
23
+
24
+ try {
25
+ generateDocs(
26
+ targetRoot,
27
+ {
28
+ frontend: 'react',
29
+ db: 'prisma',
30
+ dockerEnabled: true,
31
+ i18nEnabled: false,
32
+ proxy: 'none',
33
+ },
34
+ packageRoot,
35
+ );
36
+
37
+ const readme = readFile(path.join(targetRoot, 'README.md'));
38
+ const projectDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'PROJECT.md'));
39
+ const architectureDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'ARCHITECTURE.md'));
40
+
41
+ assert.match(readme, /Docker\/infra: `enabled`/);
42
+ assert.match(readme, /Quick Start \(Docker\)/);
43
+ assert.match(readme, /Proxy Preset: none/);
44
+ assert.doesNotMatch(readme, /i18n Configuration/);
45
+
46
+ assert.match(projectDoc, /### Docker mode/);
47
+ assert.match(projectDoc, /Active proxy preset: `none`/);
48
+ assert.doesNotMatch(projectDoc, /packages\/i18n/);
49
+
50
+ assert.match(architectureDoc, /infra\/\*/);
51
+ assert.doesNotMatch(architectureDoc, /I18N_ENABLED/);
52
+ } finally {
53
+ fs.rmSync(targetRoot, { recursive: true, force: true });
54
+ }
55
+ });
56
+
57
+ it('generates docker and caddy notes when enabled', () => {
58
+ const targetRoot = makeTempDir('forgeon-docs-on-');
59
+
60
+ try {
61
+ generateDocs(
62
+ targetRoot,
63
+ {
64
+ frontend: 'react',
65
+ db: 'prisma',
66
+ dockerEnabled: true,
67
+ i18nEnabled: true,
68
+ proxy: 'caddy',
69
+ },
70
+ packageRoot,
71
+ );
72
+
73
+ const readme = readFile(path.join(targetRoot, 'README.md'));
74
+ const projectDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'PROJECT.md'));
75
+ const architectureDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'ARCHITECTURE.md'));
76
+
77
+ assert.match(readme, /Quick Start \(Docker\)/);
78
+ assert.match(readme, /Proxy Preset: Caddy/);
79
+ assert.match(readme, /i18n Configuration/);
80
+
81
+ assert.match(projectDoc, /`infra` - Docker Compose \(always\) \+ proxy preset \(`caddy`\)/);
82
+ assert.match(projectDoc, /Main proxy config: `infra\/caddy\/Caddyfile`/);
83
+
84
+ assert.match(architectureDoc, /infra\/\*/);
85
+ assert.match(architectureDoc, /I18N_ENABLED/);
86
+ assert.match(architectureDoc, /Active reverse proxy preset: `caddy`/);
87
+ } finally {
88
+ fs.rmSync(targetRoot, { recursive: true, force: true });
89
+ }
90
+ });
91
+ });
@@ -1,14 +1,14 @@
1
- import { spawnSync } from 'node:child_process';
2
-
3
- export function runInstall(targetRoot) {
4
- const pnpmCmd = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
5
- const result = spawnSync(pnpmCmd, ['install'], {
6
- cwd: targetRoot,
7
- stdio: 'inherit',
8
- shell: false,
9
- });
10
-
11
- if (result.status !== 0) {
12
- process.exit(result.status ?? 1);
13
- }
14
- }
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ export function runInstall(targetRoot) {
4
+ const pnpmCmd = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
5
+ const result = spawnSync(pnpmCmd, ['install'], {
6
+ cwd: targetRoot,
7
+ stdio: 'inherit',
8
+ shell: false,
9
+ });
10
+
11
+ if (result.status !== 0) {
12
+ process.exit(result.status ?? 1);
13
+ }
14
+ }
@@ -3,55 +3,58 @@ import path from 'node:path';
3
3
  import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
4
  import { toKebabCase } from '../utils/values.mjs';
5
5
  import { applyI18nDisabled, applyProxyPreset, patchDockerEnvForI18n } from '../presets/index.mjs';
6
+ import { applyModulePreset } from '../modules/executor.mjs';
6
7
  import { generateDocs } from './docs.mjs';
7
-
8
- function writeApiEnvExample(targetRoot, i18nEnabled) {
9
- const apiEnvExamplePath = path.join(targetRoot, 'apps', 'api', '.env.example');
10
- const apiEnvLines = [
11
- 'PORT=3000',
12
- 'DATABASE_URL=postgresql://postgres:postgres@localhost:5432/app?schema=public',
13
- ];
14
-
15
- if (i18nEnabled) {
16
- apiEnvLines.push('I18N_ENABLED=true');
17
- apiEnvLines.push('I18N_DEFAULT_LANG=en');
18
- apiEnvLines.push('I18N_FALLBACK_LANG=en');
19
- }
20
-
21
- fs.writeFileSync(apiEnvExamplePath, `${apiEnvLines.join('\n')}\n`, 'utf8');
22
- }
23
-
24
- function patchRootPackageJson(targetRoot, projectName) {
25
- const rootPackageJsonPath = path.join(targetRoot, 'package.json');
26
- const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, 'utf8'));
27
- rootPackageJson.name = toKebabCase(projectName);
28
-
29
- if (rootPackageJson.scripts) {
30
- delete rootPackageJson.scripts['create:forgeon'];
31
- }
32
-
33
- writeJson(rootPackageJsonPath, rootPackageJson);
34
- }
35
-
36
- export function scaffoldProject({
37
- templateRoot,
38
- packageRoot,
39
- targetRoot,
40
- projectName,
41
- frontend,
42
- db,
43
- i18nEnabled,
44
- proxy,
45
- }) {
8
+
9
+ function writeApiEnvExample(targetRoot, i18nEnabled) {
10
+ const apiEnvExamplePath = path.join(targetRoot, 'apps', 'api', '.env.example');
11
+ const apiEnvLines = [
12
+ 'PORT=3000',
13
+ 'DATABASE_URL=postgresql://postgres:postgres@localhost:5432/app?schema=public',
14
+ ];
15
+
16
+ if (i18nEnabled) {
17
+ apiEnvLines.push('I18N_ENABLED=true');
18
+ apiEnvLines.push('I18N_DEFAULT_LANG=en');
19
+ apiEnvLines.push('I18N_FALLBACK_LANG=en');
20
+ }
21
+
22
+ fs.writeFileSync(apiEnvExamplePath, `${apiEnvLines.join('\n')}\n`, 'utf8');
23
+ }
24
+
25
+ function patchRootPackageJson(targetRoot, projectName) {
26
+ const rootPackageJsonPath = path.join(targetRoot, 'package.json');
27
+ const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, 'utf8'));
28
+ rootPackageJson.name = toKebabCase(projectName);
29
+
30
+ if (rootPackageJson.scripts) {
31
+ delete rootPackageJson.scripts['create:forgeon'];
32
+ }
33
+
34
+ writeJson(rootPackageJsonPath, rootPackageJson);
35
+ }
36
+
37
+ export function scaffoldProject({
38
+ templateRoot,
39
+ packageRoot,
40
+ targetRoot,
41
+ projectName,
42
+ frontend,
43
+ db,
44
+ i18nEnabled,
45
+ proxy,
46
+ }) {
46
47
  copyRecursive(templateRoot, targetRoot);
47
48
  patchRootPackageJson(targetRoot, projectName);
48
49
  applyProxyPreset(targetRoot, proxy);
49
- patchDockerEnvForI18n(targetRoot, i18nEnabled);
50
50
 
51
- if (!i18nEnabled) {
51
+ if (i18nEnabled) {
52
+ applyModulePreset({ moduleId: 'i18n', targetRoot, packageRoot });
53
+ } else {
54
+ patchDockerEnvForI18n(targetRoot, i18nEnabled);
52
55
  applyI18nDisabled(targetRoot);
53
56
  }
54
-
55
- writeApiEnvExample(targetRoot, i18nEnabled);
56
- generateDocs(targetRoot, { frontend, db, dockerEnabled: true, i18nEnabled, proxy }, packageRoot);
57
- }
57
+
58
+ writeApiEnvExample(targetRoot, i18nEnabled);
59
+ generateDocs(targetRoot, { frontend, db, dockerEnabled: true, i18nEnabled, proxy }, packageRoot);
60
+ }
@@ -1,12 +1,12 @@
1
- import { ensureDatabaseSupported } from '../databases/index.mjs';
2
- import { ensureFrontendSupported } from '../frameworks/index.mjs';
3
- import { ensureProxySupported } from '../infrastructure/proxy.mjs';
4
-
5
- export function validatePresetSupport({ frontend, db, dockerEnabled, proxy }) {
6
- ensureFrontendSupported(frontend);
7
- ensureDatabaseSupported(db);
8
-
9
- if (dockerEnabled) {
10
- ensureProxySupported(proxy);
11
- }
12
- }
1
+ import { ensureDatabaseSupported } from '../databases/index.mjs';
2
+ import { ensureFrontendSupported } from '../frameworks/index.mjs';
3
+ import { ensureProxySupported } from '../infrastructure/proxy.mjs';
4
+
5
+ export function validatePresetSupport({ frontend, db, dockerEnabled, proxy }) {
6
+ ensureFrontendSupported(frontend);
7
+ ensureDatabaseSupported(db);
8
+
9
+ if (dockerEnabled) {
10
+ ensureProxySupported(proxy);
11
+ }
12
+ }