diff --git a/pkg/action/chart_ts_init.go b/pkg/action/chart_ts_init.go index 374a9a18..37def564 100644 --- a/pkg/action/chart_ts_init.go +++ b/pkg/action/chart_ts_init.go @@ -12,9 +12,10 @@ import ( ) type ChartTSInitOptions struct { - ChartDirPath string - ChartName string - TempDirPath string + ChartDirPath string + ChartName string + RenderContextType string + TempDirPath string } func ChartTSInit(ctx context.Context, opts ChartTSInitOptions) error { @@ -47,7 +48,9 @@ func ChartTSInit(ctx context.Context, opts ChartTSInitOptions) error { return fmt.Errorf("init chart structure: %w", err) } - if err := ts.InitTSBoilerplate(ctx, absPath, chartName); err != nil { + if err := ts.InitTSBoilerplate(ctx, absPath, chartName, ts.InitTSBoilerplateOptions{ + RenderContextType: opts.RenderContextType, + }); err != nil { return fmt.Errorf("init TypeScript boilerplate: %w", err) } diff --git a/pkg/common/common.go b/pkg/common/common.go index 0fd319d0..7ca7f89c 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -122,6 +122,11 @@ const ( StageStartSuffix = "start" StubReleaseName = "stub-release" StubReleaseNamespace = "stub-namespace" + TSDefaultRenderContextType = TSGenericRenderContextType + // TSGenericRenderContextType is the TypeScript render context type name for nelm charts. + TSGenericRenderContextType = "RenderContext" + // TSWerfRenderContextType is the TypeScript render context type name for werf charts. + TSWerfRenderContextType = "WerfRenderContext" ) var ( diff --git a/pkg/ts/init.go b/pkg/ts/init.go index c5dcdade..ab394f54 100644 --- a/pkg/ts/init.go +++ b/pkg/ts/init.go @@ -1,11 +1,13 @@ package ts import ( + "bytes" "context" "fmt" "os" "path/filepath" "strings" + "text/template" "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/log" @@ -13,6 +15,16 @@ import ( const denoBuildScript = "deno bundle --output=dist/bundle.js src/index.ts" +type InitTSBoilerplateOptions struct { + RenderContextType string +} + +type initTmplData struct { + BuildScript string + ChartName string + RenderContextType string +} + // EnsureGitignore adds TypeScript entries to .gitignore, creating if needed. func EnsureGitignore(chartPath string) error { entries := []string{ @@ -53,8 +65,7 @@ func InitChartStructure(ctx context.Context, chartPath, chartName string) error return nil } -// InitTSBoilerplate creates TypeScript boilerplate files in ts/ directory. -func InitTSBoilerplate(ctx context.Context, chartPath, chartName string) error { +func InitTSBoilerplate(ctx context.Context, chartPath, chartName string, opts InitTSBoilerplateOptions) error { tsDir := filepath.Join(chartPath, common.ChartTSSourceDir) srcDir := filepath.Join(tsDir, "src") @@ -64,17 +75,28 @@ func InitTSBoilerplate(ctx context.Context, chartPath, chartName string) error { return fmt.Errorf("stat %s: %w", tsDir, err) } + ctxType := common.TSDefaultRenderContextType + if opts.RenderContextType != "" { + ctxType = opts.RenderContextType + } + + data := initTmplData{ + BuildScript: denoBuildScript, + ChartName: chartName, + RenderContextType: ctxType, + } + files := []struct { - content string - path string + tmpl string + path string }{ - {content: indexTSContent, path: filepath.Join(srcDir, "index.ts")}, - {content: helpersTSContent, path: filepath.Join(srcDir, "helpers.ts")}, - {content: deploymentTSContent, path: filepath.Join(srcDir, "deployment.ts")}, - {content: serviceTSContent, path: filepath.Join(srcDir, "service.ts")}, - {content: tsconfigContent, path: filepath.Join(tsDir, "tsconfig.json")}, - {content: fmt.Sprintf(denoJSONTmpl, denoBuildScript), path: filepath.Join(tsDir, "deno.json")}, - {content: fmt.Sprintf(inputExampleContent, chartName), path: filepath.Join(tsDir, "input.example.yaml")}, + {tmpl: indexTSTmpl, path: filepath.Join(srcDir, "index.ts")}, + {tmpl: helpersTSTmpl, path: filepath.Join(srcDir, "helpers.ts")}, + {tmpl: deploymentTSTmpl, path: filepath.Join(srcDir, "deployment.ts")}, + {tmpl: serviceTSTmpl, path: filepath.Join(srcDir, "service.ts")}, + {tmpl: tsconfigContent, path: filepath.Join(tsDir, "tsconfig.json")}, + {tmpl: denoJSONTmpl, path: filepath.Join(tsDir, "deno.json")}, + {tmpl: inputExampleTmpl, path: filepath.Join(tsDir, "input.example.yaml")}, } if err := os.MkdirAll(srcDir, 0o755); err != nil { @@ -82,7 +104,12 @@ func InitTSBoilerplate(ctx context.Context, chartPath, chartName string) error { } for _, f := range files { - if err := os.WriteFile(f.path, []byte(f.content), 0o644); err != nil { + content, err := renderTemplate(f.tmpl, data) + if err != nil { + return fmt.Errorf("render template for %s: %w", f.path, err) + } + + if err := os.WriteFile(f.path, []byte(content), 0o644); err != nil { return fmt.Errorf("write %s: %w", f.path, err) } @@ -181,3 +208,17 @@ func fileExists(path string) (bool, error) { return false, fmt.Errorf("stat %s: %w", path, err) } + +func renderTemplate(tmplStr string, data initTmplData) (string, error) { + t, err := template.New("").Parse(tmplStr) + if err != nil { + return "", fmt.Errorf("parse template: %w", err) + } + + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { + return "", fmt.Errorf("execute template: %w", err) + } + + return buf.String(), nil +} diff --git a/pkg/ts/init_templates.go b/pkg/ts/init_templates.go index 273b0777..e1eeb17d 100644 --- a/pkg/ts/init_templates.go +++ b/pkg/ts/init_templates.go @@ -3,17 +3,17 @@ package ts const ( denoJSONTmpl = `{ "tasks": { - "build": "%s" + "build": "{{ .BuildScript }}" }, "imports": { - "@nelm/chart-ts-sdk": "npm:@nelm/chart-ts-sdk@^0.1.3" + "@nelm/chart-ts-sdk": "npm:@nelm/chart-ts-sdk@^0.1.4" } } ` - deploymentTSContent = `import type { RenderContext } from '@nelm/chart-ts-sdk'; + deploymentTSTmpl = `import type { {{ .RenderContextType }} } from '@nelm/chart-ts-sdk'; import { getFullname, getLabels, getSelectorLabels } from './helpers.ts'; -export function newDeployment($: RenderContext): object { +export function newDeployment($: {{ .RenderContextType }}): object { const name = getFullname($); return { @@ -59,7 +59,7 @@ export function newDeployment($: RenderContext): object { ts/vendor/ ts/node_modules/ ` - helpersTSContent = `import type { RenderContext } from '@nelm/chart-ts-sdk'; + helpersTSTmpl = `import type { {{ .RenderContextType }} } from '@nelm/chart-ts-sdk'; /** * Truncate string to max length, removing trailing hyphens. @@ -73,7 +73,7 @@ export function trunc(str: string, max: number): string { * Get the fully qualified app name. * Truncated at 63 chars (DNS naming spec limit). */ -export function getFullname($: RenderContext): string { +export function getFullname($: {{ .RenderContextType }}): string { if ($.Values.fullnameOverride) { return trunc($.Values.fullnameOverride, 63); } @@ -87,25 +87,25 @@ export function getFullname($: RenderContext): string { return trunc(` + "`${$.Release.Name}-${chartName}`" + `, 63); } -export function getLabels($: RenderContext): Record { +export function getLabels($: {{ .RenderContextType }}): Record { return { 'app.kubernetes.io/name': $.Chart.Name, 'app.kubernetes.io/instance': $.Release.Name, }; } -export function getSelectorLabels($: RenderContext): Record { +export function getSelectorLabels($: {{ .RenderContextType }}): Record { return { 'app.kubernetes.io/name': $.Chart.Name, 'app.kubernetes.io/instance': $.Release.Name, }; } ` - indexTSContent = `import { RenderContext, RenderResult, runRender } from '@nelm/chart-ts-sdk'; + indexTSTmpl = `import { {{ .RenderContextType }}, RenderResult, render } from '@nelm/chart-ts-sdk'; import { newDeployment } from './deployment.ts'; import { newService } from './service.ts'; -function render($: RenderContext): RenderResult { +function generate($: {{ .RenderContextType }}): RenderResult { const manifests: object[] = []; manifests.push(newDeployment($)); @@ -117,9 +117,9 @@ function render($: RenderContext): RenderResult { return { manifests }; } -await runRender(render); +await render(generate); ` - inputExampleContent = `Capabilities: + inputExampleTmpl = `Capabilities: APIVersions: - v1 HelmVersion: @@ -134,20 +134,20 @@ Chart: Annotations: anno: value AppVersion: 1.0.0 - Condition: %[1]s.enabled - Description: %[1]s description + Condition: {{ .ChartName }}.enabled + Description: {{ .ChartName }} description Home: https://example.org/home Icon: https://example.org/icon Keywords: - - %[1]s + - {{ .ChartName }} Maintainers: - Email: john@example.com Name: john URL: https://example.com/john - Name: %[1]s + Name: {{ .ChartName }} Sources: - - https://example.org/%[1]s - Tags: %[1]s + - https://example.org/{{ .ChartName }} + Tags: {{ .ChartName }} Type: application Version: 0.1.0 Files: @@ -155,8 +155,8 @@ Files: Release: IsInstall: false IsUpgrade: true - Name: %[1]s - Namespace: %[1]s + Name: {{ .ChartName }} + Namespace: {{ .ChartName }} Revision: 2 Service: Helm Values: @@ -169,10 +169,10 @@ Values: port: 80 type: ClusterIP ` - serviceTSContent = `import type { RenderContext } from '@nelm/chart-ts-sdk'; + serviceTSTmpl = `import type { {{ .RenderContextType }} } from '@nelm/chart-ts-sdk'; import { getFullname, getLabels, getSelectorLabels } from './helpers.ts'; -export function newService($: RenderContext): object { +export function newService($: {{ .RenderContextType }}): object { return { apiVersion: 'v1', kind: 'Service', diff --git a/pkg/ts/init_test.go b/pkg/ts/init_test.go index fec9ff25..4eb2303c 100644 --- a/pkg/ts/init_test.go +++ b/pkg/ts/init_test.go @@ -176,16 +176,14 @@ func TestInitTSBoilerplate(t *testing.T) { chartPath := filepath.Join(t.TempDir(), "test-chart") require.NoError(t, os.MkdirAll(chartPath, 0o755)) - err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart") + err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart", ts.InitTSBoilerplateOptions{}) require.NoError(t, err) - // Check ts/src/ files assert.FileExists(t, filepath.Join(chartPath, "ts", "src", "index.ts")) assert.FileExists(t, filepath.Join(chartPath, "ts", "src", "helpers.ts")) assert.FileExists(t, filepath.Join(chartPath, "ts", "src", "deployment.ts")) assert.FileExists(t, filepath.Join(chartPath, "ts", "src", "service.ts")) - // Check ts/ root files assert.FileExists(t, filepath.Join(chartPath, "ts", "tsconfig.json")) assert.FileExists(t, filepath.Join(chartPath, "ts", "deno.json")) assert.FileExists(t, filepath.Join(chartPath, "ts", "input.example.yaml")) @@ -195,7 +193,7 @@ func TestInitTSBoilerplate(t *testing.T) { chartPath := filepath.Join(t.TempDir(), "test-chart") require.NoError(t, os.MkdirAll(chartPath, 0o755)) - err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart") + err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart", ts.InitTSBoilerplateOptions{}) require.NoError(t, err) assert.DirExists(t, filepath.Join(chartPath, "ts")) @@ -206,7 +204,7 @@ func TestInitTSBoilerplate(t *testing.T) { chartPath := filepath.Join(t.TempDir(), "my-custom-chart") require.NoError(t, os.MkdirAll(chartPath, 0o755)) - err := ts.InitTSBoilerplate(context.Background(), chartPath, "my-custom-chart") + err := ts.InitTSBoilerplate(context.Background(), chartPath, "my-custom-chart", ts.InitTSBoilerplateOptions{}) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(chartPath, "ts", "deno.json")) @@ -215,26 +213,44 @@ func TestInitTSBoilerplate(t *testing.T) { assert.Contains(t, string(content), `"@nelm/chart-ts-sdk"`) }) - t.Run("includes render function in index.ts", func(t *testing.T) { + t.Run("uses RenderContext by default", func(t *testing.T) { chartPath := filepath.Join(t.TempDir(), "test-chart") require.NoError(t, os.MkdirAll(chartPath, 0o755)) - err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart") + err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart", ts.InitTSBoilerplateOptions{}) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(chartPath, "ts", "src", "index.ts")) require.NoError(t, err) - assert.Contains(t, string(content), "function render") + assert.Contains(t, string(content), "function generate") assert.Contains(t, string(content), "RenderContext") assert.Contains(t, string(content), "RenderResult") - assert.Contains(t, string(content), "runRender") + assert.Contains(t, string(content), "await render(generate)") + assert.NotContains(t, string(content), "WerfRenderContext") + }) + + t.Run("uses custom render context type when specified", func(t *testing.T) { + chartPath := filepath.Join(t.TempDir(), "test-chart") + require.NoError(t, os.MkdirAll(chartPath, 0o755)) + + err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart", ts.InitTSBoilerplateOptions{ + RenderContextType: "WerfRenderContext", + }) + require.NoError(t, err) + + for _, file := range []string{"index.ts", "helpers.ts", "deployment.ts", "service.ts"} { + content, err := os.ReadFile(filepath.Join(chartPath, "ts", "src", file)) + require.NoError(t, err) + assert.Contains(t, string(content), "WerfRenderContext", "file %s should use WerfRenderContext", file) + assert.NotContains(t, string(content), "import type { RenderContext }", "file %s should not import RenderContext", file) + } }) t.Run("includes helper functions in helpers.ts", func(t *testing.T) { chartPath := filepath.Join(t.TempDir(), "test-chart") require.NoError(t, os.MkdirAll(chartPath, 0o755)) - err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart") + err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart", ts.InitTSBoilerplateOptions{}) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(chartPath, "ts", "src", "helpers.ts")) @@ -248,7 +264,7 @@ func TestInitTSBoilerplate(t *testing.T) { chartPath := filepath.Join(t.TempDir(), "test-chart") require.NoError(t, os.MkdirAll(chartPath, 0o755)) - err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart") + err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart", ts.InitTSBoilerplateOptions{}) require.NoError(t, err) deploymentContent, err := os.ReadFile(filepath.Join(chartPath, "ts", "src", "deployment.ts")) @@ -264,7 +280,7 @@ func TestInitTSBoilerplate(t *testing.T) { chartPath := filepath.Join(t.TempDir(), "test-chart") require.NoError(t, os.MkdirAll(chartPath, 0o755)) - err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart") + err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart", ts.InitTSBoilerplateOptions{}) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(chartPath, "ts", "deno.json")) @@ -276,7 +292,7 @@ func TestInitTSBoilerplate(t *testing.T) { chartPath := filepath.Join(t.TempDir(), "test-chart") require.NoError(t, os.MkdirAll(chartPath, 0o755)) - err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart") + err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart", ts.InitTSBoilerplateOptions{}) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(chartPath, "ts", "tsconfig.json")) @@ -291,7 +307,7 @@ func TestInitTSBoilerplate(t *testing.T) { chartPath := filepath.Join(t.TempDir(), "my-custom-chart") require.NoError(t, os.MkdirAll(chartPath, 0o755)) - err := ts.InitTSBoilerplate(context.Background(), chartPath, "my-custom-chart") + err := ts.InitTSBoilerplate(context.Background(), chartPath, "my-custom-chart", ts.InitTSBoilerplateOptions{}) require.NoError(t, err) content, err := os.ReadFile(filepath.Join(chartPath, "ts", "input.example.yaml")) @@ -307,7 +323,7 @@ func TestInitTSBoilerplate(t *testing.T) { tsDir := filepath.Join(chartPath, "ts") require.NoError(t, os.MkdirAll(tsDir, 0o755)) - err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart") + err := ts.InitTSBoilerplate(context.Background(), chartPath, "test-chart", ts.InitTSBoilerplateOptions{}) require.Error(t, err) assert.Contains(t, err.Error(), "typescript directory already exists") })