Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add README, and basic example #22

Merged
merged 3 commits into from
Mar 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
> [!CAUTION]
>
> # THIS LIBRARY IS CURRENTLY PRE-RELEASE
>
> `pkl-typescript` is currently major version `v0`, and **breaking changes will happen** between versions.
>
> Please read the section [Roadmap](#roadmap) below to learn more.

# Pkl Bindings for TypeScript

This library exposes TypeScript language bindings for the Pkl configuration language.

These language bindings are made up of:

- an "evaluator", that can execute Pkl code and deserialise the result into JavaScript runtime objects
- the `pkl-gen-typescript` CLI, that can analyse a Pkl schema and generate matching TypeScript definitions

Together, this allows you to embed Pkl into your TypeScript application, complete with code generation for full type safety and ease of use.

## Getting Started

First, install `pkl-typescript` from NPM:

```bash
npm install @pkl-community/pkl-typescript
```

Then, generate a TypeScript file from your Pkl schema (eg. for a file called "config.pkl"):

```bash
npx pkl-gen-typescript config.pkl -o ./generated
```

Lastly, in your TypeScript code, you can import the generated types and loader code:

```typescript
import { type Config, loadFromPath } from "./generated/config.pkl.ts";

const config: Config = await loadFromPath("config.pkl");
```

See more example usage in the [examples directory](./examples/).

### Note on Schemas vs. Configs

`pkl-gen-typescript` generates a TypeScript file based on Pkl's **type information** only, _not_ Pkl's runtime values. For example, a Pkl file with `x: String = "hello"` would produce the TypeScript type `x: string`.
Conversely, the evaluator (used by the `loadFromPath(string)` function) evaluates a Pkl module that **renders values**.

You may choose to have your Pkl schemas and values defined in separate Pkl files (eg. `schema.pkl` and `config.pkl`, where `config.pkl` starts with `amends "schema.pkl"`). In such a case, you would pass `schema.pkl` to `pkl-gen-typescript`, but then evaluate `config.pkl` at runtime (ie. `await loadFromPath("config.pkl")`).

## Roadmap

This library is currently in pre-release: we believe it is usable and productive in its current state, but not feature-complete, and not yet API-stable.

We will keep the major version at `v0` until we are ready to commit to stability in:

- the evaluator API (as provided by the `@pkl-community/pkl-typescript` NPM package)
- the TypeScript type definitions generated by `pkl-gen-typescript`

Until then, minor and patch releases may contain breaking changes.

> [!WARNING]
> **We strongly recommend** you regenerate your generated TypeScript code (with `pkl-gen-typescript`) **every time you upgrade** `@pkl-community/pkl-typescript`. If you don't, you may end up with unexpected runtime errors from type mismatches.

### Known Current Limitations

- **Inlined imports**: Imported Pkl types are inlined into the output TypeScript file. For example, if `foo.pkl` has an import like `import "bar.pkl"`, and you run `pkl-gen-typescript foo.pkl`, the resulting `foo.pkl.ts` file will include all types defined in `foo.pkl` _as well as_ all types defined in `bar.pkl`. This means that the resulting TypeScript generated files (in a multi-file codegen) will match the set of input root files, not the file structure of the source Pkl files. This behaviour may create unintended name conflicts; these can be resolved using the `@typescript.Name { value = "..." }` annotation. It may also cause duplication (eg. if the same shared Pkl library file is imported in two schemas); TypeScript's structural typing (where equivalent type shapes can be used interchangeably) should mean that any duplicate types can be safely used as each other.
- **Regex deserialisation**: Pkl's `Regex` type will be decoded as a `pklTypescript.Regex` object, which contains a `.pattern` property. Pkl uses Java's regular expression syntax, which may not always be perfectly compatible with JavaScript's regular expression syntax. If you want to use your Pkl `Regex` as a JavaScript `RegExp`, and you are confident that the expression will behave the same way in JavaScript as in Pkl, you can instantiate a new `RegExp` using the `pklTypescript.Regex.pattern` property, eg. `const myConfigRegexp = new RegExp(myConfig.someRegex.pattern)`.
- **IntSeq deserialisation**: Pkl's `IntSeq` type is intended to be used internally within a Pkl program to create a range loop. It is unlikely to be useful as a property type in JavaScript, and is therefore decoded into a custom `pklTypescript.IntSeq` type with signature `{ start: number; end: number: step: number }` - it is _not_ decoded into an array containing the ranged values. If you have a use-case to use `IntSeq` as an array of ranged values in a TypeScript program, please file a GitHub Issue.
- **Duration and DataSize APIs**: Pkl has a rich API for many of its custom types, but two of note (that are not common in standard libraries of other languages) are `Duration` and `DataSize`, which include convenience APIs for eg. converting between units or summing values. These types are decoded into `pklTypescript.DataSize`/`pklTypescript.Duration` types (each of which have a `value` and `unit` property), and do not yet have the convenience APIs from Pkl.

## Appendix

### Type Mappings

When code-generating TypeScript type definitions from Pkl schemas, each Pkl type is converted to an associated TypeScript type, as per the table below. While in pre-release, these mappings are subject to change!

| Pkl type | TypeScript type |
| ---------------- | -------------------------- |
| Null | `null` |
| Boolean | `boolean` |
| String | `string` |
| Int | `number` |
| Int8 | `number` |
| Int16 | `number` |
| Int32 | `number` |
| UInt | `number` |
| UInt8 | `number` |
| UInt16 | `number` |
| UInt32 | `number` |
| Float | `number` |
| Number | `number` |
| List<T> | `Array<T>` |
| Listing<T> | `Array<T>` |
| Map<K, V> | `Map<K, V>` |
| Mapping<K, V> | `Map<K, V>` |
| Set<T> | `Set<T>` |
| Pair<A, B> | `pklTypescript.Pair<A, B>` |
| Dynamic | `pklTypescript.Dynamic` |
| DataSize | `pklTypescript.DataSize` |
| Duration | `pklTypescript.Duration` |
| IntSeq | `pklTypescript.IntSeq` |
| Class | `interface` |
| TypeAlias | `typealias` |
| Any | `pklTypescript.Any` |
| Unions (A\|B\|C) | `A\|B\|C` |
| Regex | `pklTypescript.Regex` |
3 changes: 3 additions & 0 deletions examples/basic-intro/config.pkl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
firstName: String = "Phillip"
lastName: String = "Pklton"
age: Int8 = 76
26 changes: 26 additions & 0 deletions examples/basic-intro/generated/config.pkl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// This file was generated by `pkl-typescript` from Pkl module `config`.
// DO NOT EDIT.
import * as pklTypescript from "@pkl-community/pkl-typescript"

// Ref: Module root.
export interface Config {
firstName: string

lastName: string

age: number
}

// LoadFromPath loads the pkl module at the given path and evaluates it into a Config
export const loadFromPath = async (path: string): Promise<Config> => {
const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions);
try {
const result = await load(evaluator, pklTypescript.FileSource(path));
return result
} finally {
evaluator.close()
}
};

export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise<Config> =>
evaluator.evaluateModule(source) as Promise<Config>;
10 changes: 10 additions & 0 deletions examples/basic-intro/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { type Config, loadFromPath } from "./generated/config.pkl";

const main = async () => {
const config: Config = await loadFromPath("config.pkl");
console.log(
`Hello, ${config.firstName} ${config.lastName}! I hear you are ${config.age} years old.`
);
};

main();
Loading
Loading