-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Problem
Currently, our generators (PackageGenerator, AppGenerator, ServiceGenerator) use hardcoded strings to generate configuration files like package.json, tsconfig.json, tsup.config.ts, etc. This creates several issues:
-
Duplication: We maintain the same configuration in multiple places:
- NodeSpec's own configuration files
- Hardcoded strings in generator classes
-
Drift: When we update NodeSpec's configuration (e.g., add a new script, change tsup settings), we must remember to update the generators too. This is error-prone.
-
Inconsistency: Generated projects might use different configurations than NodeSpec itself.
Proposed Solution
"Eat our own dog food" - Use NodeSpec's actual configuration files as templates instead of hardcoding them.
Architecture
// Base class for all generators
class BaseGenerator {
/**
* Get the root directory of NodeSpec project
* This is the source of truth for all templates
*/
protected getNodeSpecRoot(): string {
// When running from published CLI: point to embedded templates
// When running from source: point to NodeSpec repo root
}
/**
* Read a template file from NodeSpec's own files
*/
protected async readTemplate(relativePath: string): Promise<string> {
const nodespecRoot = this.getNodeSpecRoot();
return fs.readFile(path.join(nodespecRoot, relativePath), 'utf-8');
}
/**
* Read and parse a JSON template
*/
protected async readJsonTemplate(relativePath: string): Promise<any> {
const content = await this.readTemplate(relativePath);
return JSON.parse(content);
}
}Template Source Mapping
| Generator | Template Source | Files to Use |
|---|---|---|
| PackageGenerator | packages/logger/ or packages/template/ |
package.json, tsconfig.json, tsup.config.ts |
| AppGenerator | apps/cli/ |
package.json, tsconfig.json, tsup.config.ts, src/ structure |
| ServiceGenerator | services/example/ (to be created) |
package.json, tsconfig.json, tsup.config.ts, src/ structure |
| ProjectGenerator | Root directory | package.json, tsconfig.json, turbo.json, .gitignore, etc. |
Implementation Plan
Phase 1: Create BaseGenerator
- Create
src/core/template/BaseGenerator.tswith template reading utilities - Implement
getNodeSpecRoot()logic:- For development: find NodeSpec repo root
- For published package: use embedded template directory
- Add helper methods:
readTemplate(),readJsonTemplate(),copyTemplate()
Phase 2: Refactor PackageGenerator
- Extend BaseGenerator
- Use
packages/template/package.jsonas template source - Only modify necessary fields (name, version, description)
- Preserve all other fields from template
- Do the same for tsconfig.json, tsup.config.ts
Phase 3: Refactor AppGenerator
- Extend BaseGenerator
- Use
apps/cli/as template source - Copy structure and configuration
- Only modify app-specific fields
Phase 4: Refactor ServiceGenerator
- Create a reference service in
services/example/ - Extend BaseGenerator
- Use the reference service as template
Phase 5: Refactor ProjectGenerator
- Use NodeSpec's root files as templates
- Simplify the massive hardcoded configuration strings
Phase 6: Template Embedding for Distribution
When publishing the CLI package, we need to embed the template files:
- Copy template source files to
dist/templates/during build - Update
getNodeSpecRoot()to detect runtime environment - Update package.json
filesfield to include templates
Benefits
- ✅ Single Source of Truth: NodeSpec's configuration is the only source
- ✅ Automatic Synchronization: Update NodeSpec config → generated projects automatically use new config
- ✅ No Duplication: Eliminate hardcoded configuration strings
- ✅ Consistency: Generated projects are guaranteed to match NodeSpec's structure
- ✅ Easier Maintenance: Only update in one place
- ✅ Better Testing: We test the exact same configuration we use
Example: Before & After
Before (PackageGenerator.ts):
private async generatePackageJson(targetDir: string, packageName: string) {
const packageJson = {
name: packageName,
version: "0.0.1",
description: `${packageName} package`,
type: "module",
main: "./dist/index.js",
// ... 50+ lines of hardcoded config
};
await fs.writeJson(path.join(targetDir, "package.json"), packageJson);
}After (PackageGenerator.ts):
private async generatePackageJson(targetDir: string, packageName: string) {
// Read NodeSpec's own template
const template = await this.readJsonTemplate('packages/template/package.json');
// Only modify necessary fields
template.name = packageName;
template.version = "0.0.1";
template.description = `${packageName} package`;
await fs.writeJson(path.join(targetDir, "package.json"), template);
}Migration Strategy
- Create
packages/template/as the canonical template package in NodeSpec - Implement BaseGenerator with template reading
- Migrate generators one by one
- Update tests to verify templates are used correctly
- Deprecate old hardcoded approach
Open Questions
-
Template Location: Should we create dedicated template directories (e.g.,
packages/template/) or use existing packages (e.g.,packages/logger/)? -
Template Customization: How do we handle fields that need computation (e.g., bin names based on package name)?
-
Template Versioning: Should templates be versioned separately from the CLI?
-
Template Distribution: Best way to bundle templates with the published CLI package?
Related Issues
- Improves consistency with #[related-issue-if-any]
- Addresses maintenance burden mentioned in #[related-issue-if-any]
Priority
Medium-High - This is a code quality and maintainability improvement that will pay dividends over time, especially as we add more features and templates.