Note: TypeScript templates are an experimental feature. To enable it, set the environment variable NELM_FEAT_TYPESCRIPT=true.

Overview

In addition to Helm templates, werf can generate Kubernetes manifests with TypeScript. Helm templates and TypeScript templates can coexist in the same chart — resulting manifests are merged into a single multi-doc YAML document.

TypeScript templates work out of the box: deploying a chart that contains a ts/ directory requires no additional tools or configuration — werf automatically downloads the Deno TypeScript runtime and renders the TypeScript templates.

Why TypeScript

Helm’s templating language works well for simple cases but becomes hard to maintain as chart complexity grows: primitive language with lots of gotchas, limited library, performance issues, debug difficulties, poor IDE/editor support and so on. TypeScript in werf solves these problems without complicating the deployment workflow.

Features

  • IDE support — full autocompletion, type checking, go-to-definition, and refactoring in any editor with Deno/TypeScript support (VS Code, JetBrains, Neovim, etc.).
  • Standard syntax — proper functions, loops, and conditionals instead of awkward template engine constructs.
  • Pure TypeScript — ts directory is a regular Deno TypeScript project, and can be render without werf, with just Deno TypeScript runtime.
  • Large ecosystem — TypeScript is one of the most popular languages with extensive documentation, community resources, and tooling.
  • Almost any third-party TypeScript/JavaScript library can be used, for example kubernetes-models, cdk8s or any other library from npm/Deno ecosystems.
  • Testing — test your code using common TypeScript libraries and tooling.
  • No extra host requirements — to deploy a TS chart all you need is werf. No need to install Node, Deno, npm, npm modules or anything else. We handle all of this for you, just do a werf converge.
  • Isolated environments — npm modules are bundled into the chart by default, and the Deno runtime can be provided by the host system, so no network calls will be done during the deployment, except to the Kubernetes itself.
  • Security — code runs in an isolated Deno sandbox with no access to the network, environment variables, or process execution. Filesystem access is limited to reading chart files.

Quick start

Initialize TypeScript files in an existing chart:

werf chart ts init

It will bootstrap the .helm/ts/ directory, which contains a TypeScript project skeleton and a few files with sample resources. Try modifying ts/src/deployment.ts — for example, change the number of replicas — then check the result:

werf render --dev

To deploy:

werf converge --dev

Chart structure

ts
src
deployment.ts
helpers.ts
index.ts
service.ts
deno.json
deno.lock
input.example.yaml
Chart.yaml
values.yaml
apiVersion: v2
name: ts-chart-example
version: 0.1.0

{
  "tasks": {
    "build": {
      "description": "Run deno build",
      "command": "deno bundle --output=dist/bundle.js src/index.ts"
    },
    "dev": {
      "description": "Run in development mode",
      "command": "deno run --no-remote --deny-read --deny-write --deny-net --deny-env --deny-run --allow-read=input.example.yaml src/index.ts --input-file ./input.example.yaml"
    },
    "start": {
      "description": "Run the bundled dist/bundle.js",
      "command": "deno run --no-remote --deny-read --deny-write --deny-net --deny-env --deny-run --allow-read=input.example.yaml dist/bundle.js --input-file ./input.example.yaml"
    }
  },
  "imports": {
    "@nelm/chart-ts-sdk": "npm:@nelm/chart-ts-sdk@^0.1.5"
  }
}

{
  "version": "5",
  "specifiers": {
    "npm:@nelm/chart-ts-sdk@~0.1.4": "0.1.4"
  },
  "npm": {
    "@jsr/std__yaml@1.0.12": {
      "integrity": "sha512-pz/BisWZWH16JvLJBwrNwUwfIsRnf9qniMrmI6Z3vIAcVRVFcA5+i4o6z6QqsMKqFzjlB66WZE+jSyujT/RvRg==",
      "tarball": "https://npm.jsr.io/~/11/@jsr/std__yaml/1.0.12.tgz"
    },
    "@nelm/chart-ts-sdk@0.1.4": {
      "integrity": "sha512-NCeflvAuZQxzmGZGpm0lP3Uy5d2xYRq8TKW0MWTKyknnZAe2tSv9+2uCZVA46oIxdm9FsBKocmCHEfqyOwe4lQ==",
      "dependencies": [
        "@std/yaml@npm:@jsr/std__yaml@1.0.12"
      ]
    }
  },
  "workspace": {
    "dependencies": [
      "npm:@nelm/chart-ts-sdk@~0.1.4"
    ]
  }
}

Capabilities:
  APIVersions:
    - v1
  HelmVersion:
    go_version: go1.25.0
    version: v3.20
  KubeVersion:
    Major: "1"
    Minor: "35"
    Version: v1.35.0
Chart:
  APIVersion: v2
  Annotations:
    anno: value
  AppVersion: 1.0.0
  Condition: ts-chart-example.enabled
  Description: ts-chart-example description
  Home: https://example.org/home
  Icon: https://example.org/icon
  Keywords:
    - ts-chart-example
  Maintainers:
    - Email: john@example.com
      Name: john
      URL: https://example.com/john
  Name: ts-chart-example
  Sources:
    - https://example.org/ts-chart-example
  Tags: ts-chart-example
  Type: application
  Version: 0.1.0
Files:
  myfile: "content"
Release:
  IsInstall: false
  IsUpgrade: true
  Name: ts-chart-example
  Namespace: ts-chart-example
  Revision: 2
  Service: Helm
Values:
  global:
    werf:
      name: myapp
      version: v2.35.0
      repo: example.org/mycompany/myapp
      env: production
      images:
        app:
          registry: example.org
          namespace: mycompany
          name: myapp
          tag: a1b2c3d4-1234567890
          digest: "sha256:abcdef1234567890"
          tag_digest: "a1b2c3d4-1234567890@sha256:abcdef1234567890"
          image: example.org/mycompany/myapp
          repository: mycompany/myapp
          ref: "example.org/mycompany/myapp:a1b2c3d4-1234567890@sha256:abcdef1234567890"
          ref_tag: "example.org/mycompany/myapp:a1b2c3d4-1234567890"
          repository_ref: "mycompany/myapp:a1b2c3d4-1234567890@sha256:abcdef1234567890"
          repository_tag: "mycompany/myapp:a1b2c3d4-1234567890"
          name_ref: "myapp:a1b2c3d4-1234567890@sha256:abcdef1234567890"
          name_tag: "myapp:a1b2c3d4-1234567890"
      commit:
        date:
          human: "2025-01-15 12:00:00 +0000"
          unix: 1736942400
        hash: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
  image:
    repository: nginx
    tag: latest
  replicaCount: 1
  service:
    enabled: true
    port: 80
    type: ClusterIP

import type { WerfRenderContext } from '@nelm/chart-ts-sdk';
import { getFullname, getLabels, getSelectorLabels } from './helpers.ts';

export function newDeployment($: WerfRenderContext): object {
  const name = getFullname($);

  return {
    apiVersion: 'apps/v1',
    kind: 'Deployment',
    metadata: {
      name,
      labels: getLabels($),
    },
    spec: {
      replicas: $.Values.replicaCount ?? 1,
      selector: {
        matchLabels: getSelectorLabels($),
      },
      template: {
        metadata: {
          labels: getSelectorLabels($),
        },
        spec: {
          containers: [
            {
              name: name,
              image: ($.Values.image?.repository ?? 'nginx') + ':' + ($.Values.image?.tag ?? 'latest'),
              ports: [
                {
                  name: 'http',
                  containerPort: $.Values.service?.port ?? 80,
                },
              ],
            },
          ],
        },
      },
    },
  };
}

import type { WerfRenderContext } from '@nelm/chart-ts-sdk';

/**
 * Truncate string to max length, removing trailing hyphens.
 */
export function trunc(str: string, max: number): string {
  if (str.length <= max) return str;
  return str.slice(0, max).replace(/-+$/, '');
}

/**
 * Get the fully qualified app name.
 * Truncated at 63 chars (DNS naming spec limit).
 */
export function getFullname($: WerfRenderContext): string {
  if ($.Values.fullnameOverride) {
    return trunc($.Values.fullnameOverride, 63);
  }

  const chartName = $.Values.nameOverride || $.Chart.Name;

  if ($.Release.Name.includes(chartName)) {
    return trunc($.Release.Name, 63);
  }

  return trunc(`${$.Release.Name}-${chartName}`, 63);
}

export function getLabels($: WerfRenderContext): Record<string, string> {
  return {
    'app.kubernetes.io/name': $.Chart.Name,
    'app.kubernetes.io/instance': $.Release.Name,
  };
}

export function getSelectorLabels($: WerfRenderContext): Record<string, string> {
  return {
    'app.kubernetes.io/name': $.Chart.Name,
    'app.kubernetes.io/instance': $.Release.Name,
  };
}

import { WerfRenderContext, RenderResult, render } from '@nelm/chart-ts-sdk';
import { newDeployment } from './deployment.ts';
import { newService } from './service.ts';

function generate($: WerfRenderContext): RenderResult {
  const manifests: object[] = [];

  manifests.push(newDeployment($));

  if ($.Values.service?.enabled !== false) {
    manifests.push(newService($));
  }

  return { manifests };
}

await render(generate);

import type { WerfRenderContext } from '@nelm/chart-ts-sdk';
import { getFullname, getLabels, getSelectorLabels } from './helpers.ts';

export function newService($: WerfRenderContext): object {
  return {
    apiVersion: 'v1',
    kind: 'Service',
    metadata: {
      name: getFullname($),
      labels: getLabels($),
    },
    spec: {
      type: $.Values.service?.type ?? 'ClusterIP',
      ports: [
        {
          port: $.Values.service?.port ?? 80,
          targetPort: 'http',
        },
      ],
      selector: getSelectorLabels($),
    },
  };
}

replicaCount: 1

image:
  repository: nginx
  tag: latest

service:
  enabled: true
  type: ClusterIP
  port: 80

Developing a chart with TypeScript templates

Install Deno and follow the setup guide for your IDE/editor (VS Code, JetBrains, Neovim, etc.).

Initialize TypeScript files in the chart if not already initialized:

werf chart ts init

Open the ts/ directory in your editor as a regular Deno/TypeScript project. You can work with it the same way you would with any TypeScript codebase — run scripts, write tests, use a debugger. Deno provides a rich set of tools for testing, linting, formatting, and more. See Deno documentation for details.

The codebase can be organized as you wish. The only requirement is that ts/src/index.ts exists, and render function from @nelm/chart-ts-sdk must be called. Otherwise, no TypeScript rendering happens.

To debug templates rendering in an environment that is very close to how werf runs Deno, you can use dev task from ts/deno.json:

cd .helm/ts
deno task dev

TypeScript engine will call render function from ts/src/index.ts with the example context from ts/input.example.yaml. The resulting YAML will be printed to the console below the Rendered manifests: message.

Install libraries using deno add, for example, try to install kubernetes-models — library for strict Kubernetes resource typing:

deno add npm:kubernetes-models

The dependency is added to deno.json automatically. Now you can import and use it:

// .helm/ts/src/deployment.ts:
import { Deployment } from 'kubernetes-models/apps/v1';

export function newDeployment($: WerfRenderContext): object {
  return new Deployment({
    metadata: { name: 'myapp' },
    spec: {
      // other fields
    },
  }).toJSON();
}

To ensure that everything actually works with the werf deno runtime, run:

werf lint --dev
werf render --dev

How to deploy a chart with TypeScript templates

Simply run werf converge: the Deno binary will be downloaded into the cache and TypeScript templates will be rendered and deployed.

Note: According to giterminism policies, all changed files must be committed.

Deploying into isolated environments

For the isolated environments, where Deno cannot be downloaded automatically:

  1. Publish the chart:
    werf bundle publish --repo example.org/mycompany/myapp
    

    All npm modules will be minified and bundled inside, so that the chart can be installed even without Internet access.

  2. On the target machine with an isolated environment (no network access), download Deno manually and run:
    werf bundle apply --repo example.org/mycompany/myapp --deno-binary-path /usr/local/bin/deno
    

    Where /usr/local/bin/deno is the path to the local Deno binary. TypeScript templates will be rendered and deployed using pre-compiled files from the chart bundle.

SDK API overview

TypeScript engine uses the @nelm/chart-ts-sdk package.

“render” and “generate” functions

index.ts must call the render() function. The function generate(), which will actually generate the manifests, should be passed to the render() function as an argument, for example:

// .helm/ts/src/index.ts:
await render(generate);

“WerfRenderContext” object

The generate function receives the root context in the $ variable of type WerfRenderContext — the same context as in Helm templates:

Field Type Description
$.Values WerfServiceValues Chart parameters + service values at $.Values.global.werf
$.Release Release Release information
$.Chart ChartMetadata Metadata from Chart.yaml
$.Capabilities Capabilities Cluster capabilities (API versions, Kubernetes version)
$.Files Record<string, Uint8Array> Raw chart files (except templates/ and ts/)

See the example context in ts/input.example.yaml. For details on parameters and how they are constructed, see Parametrize templates.

“RenderResult” object

The generate function returns RenderResult — an object with a manifests array. Each element is a plain JavaScript object representing a Kubernetes resource. Example output:

{
  "manifests": [
    {
      "apiVersion": "apps/v1",
      "kind": "Deployment",
      "metadata": { "name": "myapp" },
      "spec": { "..." }
    },
    {
      "apiVersion": "v1",
      "kind": "Service",
      "metadata": { "name": "myapp" },
      "spec": { "..." }
    }
  ]
}

Each object is serialized to YAML and included in the final rendered output.