create-forgeon 0.2.8 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/docs.mjs +14 -79
- package/src/core/docs.test.mjs +11 -36
- package/src/core/scaffold.mjs +5 -1
- package/src/integrations/flow.mjs +1 -1
- package/src/modules/docs.mjs +23 -23
- package/src/modules/executor.mjs +1 -1
- package/src/modules/executor.test.mjs +9 -5
- package/templates/docs-fragments/AI_ARCHITECTURE/40_docs_generation.md +11 -9
- package/templates/docs-fragments/README/90_next_steps.md +6 -7
package/README.md
CHANGED
|
@@ -37,4 +37,4 @@ pnpm forgeon:sync-integrations
|
|
|
37
37
|
- `add i18n` is implemented and applies backend/frontend i18n wiring.
|
|
38
38
|
- `add jwt-auth` is implemented and auto-detects DB adapter support for refresh-token persistence.
|
|
39
39
|
- Integration sync is bundled by default and runs after `add` commands (best-effort).
|
|
40
|
-
-
|
|
40
|
+
- Module notes are written under `modules/<module-id>/README.md`.
|
package/package.json
CHANGED
package/src/core/docs.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
function renderTemplate(content, variables) {
|
|
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
|
+
|
|
6
|
+
function renderTemplate(content, variables) {
|
|
8
7
|
return content.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => String(variables[key] ?? ''));
|
|
9
8
|
}
|
|
10
9
|
|
|
@@ -50,7 +49,6 @@ export function generateDocs(targetRoot, options, packageRoot) {
|
|
|
50
49
|
DB_PRISMA_STATUS: options.dbPrismaEnabled ? 'enabled' : 'disabled',
|
|
51
50
|
DOCKER_STATUS: 'enabled',
|
|
52
51
|
PROXY_LABEL: options.proxy,
|
|
53
|
-
PROXY_CONFIG_PATH: getProxyConfigPath(options.proxy),
|
|
54
52
|
};
|
|
55
53
|
|
|
56
54
|
const readmeFragments = ['00_title', '10_stack', '20_quick_start_dev_intro'];
|
|
@@ -78,76 +76,13 @@ export function generateDocs(targetRoot, options, packageRoot) {
|
|
|
78
76
|
}
|
|
79
77
|
readmeFragments.push('41_error_handling');
|
|
80
78
|
readmeFragments.push('90_next_steps');
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
aiProjectFragments.push('20b_structure_db_prisma');
|
|
85
|
-
} else {
|
|
86
|
-
aiProjectFragments.push('20b_structure_db_none');
|
|
87
|
-
}
|
|
88
|
-
if (options.i18nEnabled) {
|
|
89
|
-
aiProjectFragments.push('21_structure_i18n');
|
|
90
|
-
}
|
|
91
|
-
aiProjectFragments.push('22_structure_docker', '23_structure_docs', '30_run_dev', '31_run_docker');
|
|
92
|
-
if (options.proxy === 'none') {
|
|
93
|
-
aiProjectFragments.push('32_proxy_notes_none');
|
|
94
|
-
} else {
|
|
95
|
-
aiProjectFragments.push('32_proxy_notes');
|
|
96
|
-
}
|
|
97
|
-
if (options.i18nEnabled) {
|
|
98
|
-
aiProjectFragments.push('33_i18n_notes');
|
|
99
|
-
}
|
|
100
|
-
aiProjectFragments.push('34_error_handling');
|
|
101
|
-
aiProjectFragments.push('40_change_boundaries_base');
|
|
102
|
-
if (options.proxy !== 'none') {
|
|
103
|
-
aiProjectFragments.push('41_change_boundaries_docker');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const aiArchitectureFragments = ['00_title', '10_layout_base', '11_layout_infra'];
|
|
107
|
-
if (options.i18nEnabled) {
|
|
108
|
-
aiArchitectureFragments.push('12_layout_i18n_resources');
|
|
109
|
-
}
|
|
110
|
-
aiArchitectureFragments.push('20_env_base');
|
|
111
|
-
if (options.dbPrismaEnabled) {
|
|
112
|
-
aiArchitectureFragments.push('20b_env_db_prisma');
|
|
113
|
-
}
|
|
114
|
-
if (options.i18nEnabled) {
|
|
115
|
-
aiArchitectureFragments.push('21_env_i18n');
|
|
116
|
-
}
|
|
117
|
-
aiArchitectureFragments.push('22_ts_module_policy');
|
|
118
|
-
aiArchitectureFragments.push('23_error_handling');
|
|
119
|
-
if (options.dbPrismaEnabled) {
|
|
120
|
-
aiArchitectureFragments.push('30_default_db');
|
|
121
|
-
} else {
|
|
122
|
-
aiArchitectureFragments.push('30_default_db_none');
|
|
123
|
-
}
|
|
124
|
-
aiArchitectureFragments.push('31_docker_runtime', '32_scope_freeze');
|
|
125
|
-
aiArchitectureFragments.push('40_docs_generation', '50_extension_points');
|
|
126
|
-
|
|
127
|
-
writeDocFromFragments({
|
|
128
|
-
targetRoot,
|
|
79
|
+
|
|
80
|
+
writeDocFromFragments({
|
|
81
|
+
targetRoot,
|
|
129
82
|
outputPath: 'README.md',
|
|
130
83
|
fragmentsRoot,
|
|
131
|
-
docKey: 'README',
|
|
132
|
-
fragmentNames: readmeFragments,
|
|
133
|
-
variables,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
writeDocFromFragments({
|
|
137
|
-
targetRoot,
|
|
138
|
-
outputPath: path.join('docs', 'AI', 'PROJECT.md'),
|
|
139
|
-
fragmentsRoot,
|
|
140
|
-
docKey: 'AI_PROJECT',
|
|
141
|
-
fragmentNames: aiProjectFragments,
|
|
142
|
-
variables,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
writeDocFromFragments({
|
|
146
|
-
targetRoot,
|
|
147
|
-
outputPath: path.join('docs', 'AI', 'ARCHITECTURE.md'),
|
|
148
|
-
fragmentsRoot,
|
|
149
|
-
docKey: 'AI_ARCHITECTURE',
|
|
150
|
-
fragmentNames: aiArchitectureFragments,
|
|
151
|
-
variables,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
84
|
+
docKey: 'README',
|
|
85
|
+
fragmentNames: readmeFragments,
|
|
86
|
+
variables,
|
|
87
|
+
});
|
|
88
|
+
}
|
package/src/core/docs.test.mjs
CHANGED
|
@@ -18,7 +18,7 @@ describe('generateDocs', () => {
|
|
|
18
18
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
const packageRoot = path.resolve(thisDir, '..', '..');
|
|
20
20
|
|
|
21
|
-
it('generates docs for proxy=none without i18n section', () => {
|
|
21
|
+
it('generates docs for proxy=none without i18n section', () => {
|
|
22
22
|
const targetRoot = makeTempDir('forgeon-docs-off-');
|
|
23
23
|
|
|
24
24
|
try {
|
|
@@ -34,31 +34,18 @@ describe('generateDocs', () => {
|
|
|
34
34
|
},
|
|
35
35
|
packageRoot,
|
|
36
36
|
);
|
|
37
|
-
|
|
38
|
-
const readme = readFile(path.join(targetRoot, 'README.md'));
|
|
39
|
-
|
|
40
|
-
const architectureDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'ARCHITECTURE.md'));
|
|
41
|
-
|
|
37
|
+
|
|
38
|
+
const readme = readFile(path.join(targetRoot, 'README.md'));
|
|
39
|
+
|
|
42
40
|
assert.match(readme, /db-prisma`: `disabled`/);
|
|
43
41
|
assert.match(readme, /No DB module is enabled by default/);
|
|
44
42
|
assert.match(readme, /Quick Start \(Docker\)/);
|
|
45
43
|
assert.match(readme, /Proxy Preset: none/);
|
|
46
44
|
assert.match(readme, /Error Handling \(`core-errors`\)/);
|
|
45
|
+
assert.match(readme, /Module notes index: `modules\/README\.md`/);
|
|
47
46
|
assert.doesNotMatch(readme, /i18n Configuration/);
|
|
48
47
|
assert.doesNotMatch(readme, /Prisma In Container Start/);
|
|
49
|
-
|
|
50
|
-
assert.match(projectDoc, /### Docker mode/);
|
|
51
|
-
assert.match(projectDoc, /Active proxy preset: `none`/);
|
|
52
|
-
assert.match(projectDoc, /CoreErrorsModule/);
|
|
53
|
-
assert.doesNotMatch(projectDoc, /packages\/i18n/);
|
|
54
|
-
|
|
55
|
-
assert.match(architectureDoc, /generated without `db-prisma`/);
|
|
56
|
-
assert.doesNotMatch(architectureDoc, /I18N_ENABLED/);
|
|
57
|
-
assert.match(architectureDoc, /API_PREFIX/);
|
|
58
|
-
assert.match(architectureDoc, /Config Strategy/);
|
|
59
|
-
assert.match(architectureDoc, /TypeScript Module Policy/);
|
|
60
|
-
assert.match(architectureDoc, /tsconfig\.base\.esm\.json/);
|
|
61
|
-
assert.doesNotMatch(architectureDoc, /DbPrismaModule/);
|
|
48
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'docs')), false);
|
|
62
49
|
} finally {
|
|
63
50
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
64
51
|
}
|
|
@@ -80,29 +67,17 @@ describe('generateDocs', () => {
|
|
|
80
67
|
},
|
|
81
68
|
packageRoot,
|
|
82
69
|
);
|
|
83
|
-
|
|
84
|
-
const readme = readFile(path.join(targetRoot, 'README.md'));
|
|
85
|
-
|
|
86
|
-
const architectureDoc = readFile(path.join(targetRoot, 'docs', 'AI', 'ARCHITECTURE.md'));
|
|
87
|
-
|
|
70
|
+
|
|
71
|
+
const readme = readFile(path.join(targetRoot, 'README.md'));
|
|
72
|
+
|
|
88
73
|
assert.match(readme, /Quick Start \(Docker\)/);
|
|
89
74
|
assert.match(readme, /Proxy Preset: Caddy/);
|
|
90
75
|
assert.match(readme, /i18n Configuration/);
|
|
91
76
|
assert.match(readme, /db-prisma`: `enabled`/);
|
|
92
77
|
assert.match(readme, /Prisma In Container Start/);
|
|
93
78
|
assert.match(readme, /Error Handling \(`core-errors`\)/);
|
|
94
|
-
|
|
95
|
-
assert.
|
|
96
|
-
assert.match(projectDoc, /Main proxy config: `infra\/caddy\/Caddyfile`/);
|
|
97
|
-
assert.match(projectDoc, /CoreExceptionFilter/);
|
|
98
|
-
|
|
99
|
-
assert.match(architectureDoc, /infra\/\*/);
|
|
100
|
-
assert.match(architectureDoc, /I18N_DEFAULT_LANG/);
|
|
101
|
-
assert.doesNotMatch(architectureDoc, /I18N_ENABLED/);
|
|
102
|
-
assert.match(architectureDoc, /Active reverse proxy preset: `caddy`/);
|
|
103
|
-
assert.match(architectureDoc, /TypeScript Module Policy/);
|
|
104
|
-
assert.match(architectureDoc, /tsconfig\.base\.node\.json/);
|
|
105
|
-
assert.match(architectureDoc, /DbPrismaModule/);
|
|
79
|
+
assert.match(readme, /Module-specific notes: `modules\/<module-id>\/README\.md`/);
|
|
80
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'docs')), false);
|
|
106
81
|
} finally {
|
|
107
82
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
108
83
|
}
|
package/src/core/scaffold.mjs
CHANGED
|
@@ -38,7 +38,7 @@ function patchRootPackageJson(targetRoot, projectName) {
|
|
|
38
38
|
writeJson(rootPackageJsonPath, rootPackageJson);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export function scaffoldProject({
|
|
41
|
+
export function scaffoldProject({
|
|
42
42
|
templateRoot,
|
|
43
43
|
packageRoot,
|
|
44
44
|
targetRoot,
|
|
@@ -50,6 +50,10 @@ export function scaffoldProject({
|
|
|
50
50
|
proxy,
|
|
51
51
|
}) {
|
|
52
52
|
copyRecursive(templateRoot, targetRoot);
|
|
53
|
+
const generatedDocsPath = path.join(targetRoot, 'docs');
|
|
54
|
+
if (fs.existsSync(generatedDocsPath)) {
|
|
55
|
+
fs.rmSync(generatedDocsPath, { recursive: true, force: true });
|
|
56
|
+
}
|
|
53
57
|
patchRootPackageJson(targetRoot, projectName);
|
|
54
58
|
applyProxyPreset(targetRoot, proxy);
|
|
55
59
|
|
|
@@ -114,5 +114,5 @@ export async function runIntegrationFlow({
|
|
|
114
114
|
|
|
115
115
|
export function printModuleAdded(moduleId, docsPath) {
|
|
116
116
|
console.log(colorize('green', `✔ Module added: ${moduleId}`));
|
|
117
|
-
console.log(`-
|
|
117
|
+
console.log(`- readme: ${docsPath}`);
|
|
118
118
|
}
|
package/src/modules/docs.mjs
CHANGED
|
@@ -20,25 +20,25 @@ function readModuleFragment(packageRoot, moduleId, fragmentName, variables) {
|
|
|
20
20
|
return renderTemplate(raw, variables).trim();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function ensureModuleIndex(targetRoot) {
|
|
24
|
-
const indexPath = path.join(targetRoot, '
|
|
25
|
-
if (!fs.existsSync(indexPath)) {
|
|
26
|
-
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
27
|
-
fs.writeFileSync(
|
|
28
|
-
indexPath,
|
|
29
|
-
'#
|
|
30
|
-
'utf8',
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
return indexPath;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function updateModuleIndex(indexPath, preset) {
|
|
37
|
-
const relativePath =
|
|
38
|
-
const nextLine = `- \`${preset.id}\` - ${preset.label} (${preset.implemented ? 'implemented' : 'planned'})`;
|
|
39
|
-
const current = fs.readFileSync(indexPath, 'utf8').replace(/\r\n/g, '\n');
|
|
40
|
-
|
|
41
|
-
if (current.includes(`\`${preset.id}\``)) {
|
|
23
|
+
function ensureModuleIndex(targetRoot) {
|
|
24
|
+
const indexPath = path.join(targetRoot, 'modules', 'README.md');
|
|
25
|
+
if (!fs.existsSync(indexPath)) {
|
|
26
|
+
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
27
|
+
fs.writeFileSync(
|
|
28
|
+
indexPath,
|
|
29
|
+
'# Modules\n\nUser-facing notes for modules added via `create-forgeon add`.\n',
|
|
30
|
+
'utf8',
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return indexPath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function updateModuleIndex(indexPath, preset) {
|
|
37
|
+
const relativePath = `./${preset.id}/README.md`;
|
|
38
|
+
const nextLine = `- [\`${preset.id}\`](${relativePath}) - ${preset.label} (${preset.implemented ? 'implemented' : 'planned'})`;
|
|
39
|
+
const current = fs.readFileSync(indexPath, 'utf8').replace(/\r\n/g, '\n');
|
|
40
|
+
|
|
41
|
+
if (current.includes(`\`${preset.id}\``)) {
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -46,7 +46,7 @@ function updateModuleIndex(indexPath, preset) {
|
|
|
46
46
|
fs.writeFileSync(indexPath, content, 'utf8');
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export function writeModuleDocs({ packageRoot, targetRoot, preset }) {
|
|
49
|
+
export function writeModuleDocs({ packageRoot, targetRoot, preset }) {
|
|
50
50
|
const variables = {
|
|
51
51
|
MODULE_ID: preset.id,
|
|
52
52
|
MODULE_LABEL: preset.label,
|
|
@@ -59,9 +59,9 @@ export function writeModuleDocs({ packageRoot, targetRoot, preset }) {
|
|
|
59
59
|
.map((fragmentName) => readModuleFragment(packageRoot, preset.id, fragmentName, variables))
|
|
60
60
|
.filter(Boolean);
|
|
61
61
|
|
|
62
|
-
const outputPath = path.join(targetRoot, '
|
|
63
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
64
|
-
fs.writeFileSync(outputPath, `${sections.join('\n\n').trimEnd()}\n`, 'utf8');
|
|
62
|
+
const outputPath = path.join(targetRoot, 'modules', preset.id, 'README.md');
|
|
63
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
64
|
+
fs.writeFileSync(outputPath, `${sections.join('\n\n').trimEnd()}\n`, 'utf8');
|
|
65
65
|
|
|
66
66
|
const indexPath = ensureModuleIndex(targetRoot);
|
|
67
67
|
updateModuleIndex(indexPath, preset);
|
package/src/modules/executor.mjs
CHANGED
|
@@ -57,7 +57,7 @@ export function addModule({ moduleId, targetRoot, packageRoot, writeDocs = true
|
|
|
57
57
|
targetRoot,
|
|
58
58
|
preset,
|
|
59
59
|
})
|
|
60
|
-
: path.join(targetRoot, '
|
|
60
|
+
: path.join(targetRoot, 'modules', preset.id, 'README.md');
|
|
61
61
|
|
|
62
62
|
return {
|
|
63
63
|
preset,
|
|
@@ -289,11 +289,13 @@ describe('addModule', () => {
|
|
|
289
289
|
packageRoot,
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
-
assert.equal(result.applied, false);
|
|
293
|
-
assert.match(result.message, /planned/);
|
|
294
|
-
assert.equal(fs.existsSync(result.docsPath), true);
|
|
295
|
-
|
|
296
|
-
|
|
292
|
+
assert.equal(result.applied, false);
|
|
293
|
+
assert.match(result.message, /planned/);
|
|
294
|
+
assert.equal(fs.existsSync(result.docsPath), true);
|
|
295
|
+
assert.match(result.docsPath, /modules[\\/].+[\\/]README\.md$/);
|
|
296
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'modules', 'README.md')), true);
|
|
297
|
+
|
|
298
|
+
const note = fs.readFileSync(result.docsPath, 'utf8');
|
|
297
299
|
assert.match(note, /Queue Worker/);
|
|
298
300
|
assert.match(note, /Status: planned/);
|
|
299
301
|
} finally {
|
|
@@ -337,6 +339,8 @@ describe('addModule', () => {
|
|
|
337
339
|
proxy: 'caddy',
|
|
338
340
|
});
|
|
339
341
|
|
|
342
|
+
assert.equal(fs.existsSync(path.join(projectRoot, 'docs')), false);
|
|
343
|
+
|
|
340
344
|
const result = addModule({
|
|
341
345
|
moduleId: 'i18n',
|
|
342
346
|
targetRoot: projectRoot,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
## Docs Generation Pipeline
|
|
2
|
-
|
|
3
|
-
Project docs are assembled from markdown fragments in:
|
|
4
|
-
|
|
5
|
-
- `packages/create-forgeon/templates/docs-fragments/README`
|
|
6
|
-
- `packages/create-forgeon/templates/docs-fragments/AI_PROJECT`
|
|
7
|
-
- `packages/create-forgeon/templates/docs-fragments/AI_ARCHITECTURE`
|
|
8
|
-
|
|
9
|
-
During scaffold generation, the CLI
|
|
1
|
+
## Docs Generation Pipeline
|
|
2
|
+
|
|
3
|
+
Project docs are assembled from markdown fragments in:
|
|
4
|
+
|
|
5
|
+
- `packages/create-forgeon/templates/docs-fragments/README`
|
|
6
|
+
- `packages/create-forgeon/templates/docs-fragments/AI_PROJECT`
|
|
7
|
+
- `packages/create-forgeon/templates/docs-fragments/AI_ARCHITECTURE`
|
|
8
|
+
|
|
9
|
+
During scaffold generation, the CLI currently writes the generated project `README.md` from these fragments.
|
|
10
|
+
|
|
11
|
+
Internal architecture fragments remain in the Forgeon repository as generator source material and are not emitted into generated projects by default.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
## Next Steps
|
|
2
|
-
|
|
3
|
-
- Backend entrypoint: `apps/api/src/main.ts`
|
|
4
|
-
- Frontend entrypoint: `apps/web/src/main.tsx`
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
- Module contract spec: `docs/AI/MODULE_SPEC.md`
|
|
1
|
+
## Next Steps
|
|
2
|
+
|
|
3
|
+
- Backend entrypoint: `apps/api/src/main.ts`
|
|
4
|
+
- Frontend entrypoint: `apps/web/src/main.tsx`
|
|
5
|
+
- Module notes index: `modules/README.md`
|
|
6
|
+
- Module-specific notes: `modules/<module-id>/README.md`
|