├── VERSION.txt ├── .gitattributes ├── .prettierignore ├── codegen ├── src │ ├── tests │ │ ├── gatherer-fixtures │ │ │ ├── types3.pkl │ │ │ ├── types4.pkl │ │ │ ├── types2.pkl │ │ │ └── types.pkl │ │ ├── gatherer.test.pkl │ │ ├── ClassGen.test.pkl-expected.pcf │ │ ├── utils.test.pkl │ │ ├── ClassGen.test.pkl │ │ └── typegen.test.pkl │ ├── PklProject.deps.json │ ├── internal │ │ ├── Gen.pkl │ │ ├── TypeAliasGen.pkl │ │ ├── TypescriptMapping.pkl │ │ ├── Type.pkl │ │ ├── TypescriptModule.pkl │ │ ├── gatherer.pkl │ │ ├── typegen.pkl │ │ ├── utils.pkl │ │ └── ClassGen.pkl │ ├── GeneratorSettings.pkl │ ├── PklProject │ ├── typescript.pkl │ └── Generator.pkl └── snippet-tests │ ├── input │ ├── 05-withPair.pkl │ ├── 07-literalTypes.pkl │ ├── 10-namedMod.pkl │ ├── support │ │ └── moduleWithClass.pkl │ ├── 11-withImport.pkl │ ├── 03-nullables.pkl │ ├── 01-primitiveTypes.pkl │ ├── 09-customTypes.pkl │ ├── 06-withTypeAlias.pkl │ ├── 02-collections.pkl │ ├── 12-moduleNameOverride.pkl │ ├── 08-withUnion.pkl │ └── 04-withClass.pkl │ ├── generator-settings.pkl │ ├── test.pkl │ └── output │ ├── 05_with_pair.pkl.ts │ ├── 07_literal_types.pkl.ts │ ├── n10_pkl_typescript_tests_named_module.pkl.ts │ ├── 03_nullables.pkl.ts │ ├── 11_with_import.pkl.ts │ ├── 01_primitive_types.pkl.ts │ ├── 09_custom_types.pkl.ts │ ├── 06_with_type_alias.pkl.ts │ ├── n12_pkl_typescript_tests_module_name_override.pkl.ts │ ├── 02_collections.pkl.ts │ ├── 08_with_union.pkl.ts │ └── 04_with_class.pkl.ts ├── e2e └── only_primitives │ ├── schema.pkl │ ├── missingRequired.pkl │ ├── correct.pkl │ ├── wrongType.pkl │ ├── outOfBounds.pkl │ └── primitives.test.ts ├── .editorconfig ├── examples ├── express-server │ ├── ConfigSchema.pkl │ ├── config.dev.pkl │ ├── config.prod.pkl │ ├── package.json │ ├── index.ts │ ├── generated │ │ └── config_schema.pkl.ts │ └── README.md └── basic-intro │ ├── config.pkl │ ├── index.ts │ ├── package.json │ ├── generated │ └── config.pkl.ts │ └── package-lock.json ├── .gitignore ├── pkl-gen-typescript ├── generated │ ├── index.ts │ ├── README.md │ └── pkl_typescript_generator_settings.pkl.ts ├── generate.ts └── main.ts ├── .github ├── dependabot.yml └── workflows │ ├── publish.yaml │ ├── lint.yaml │ └── test.yaml ├── .prettierrc ├── bin └── pkl-gen-typescript.ts ├── tsconfig.dist.json ├── jest.config.js ├── tsconfig.json ├── src ├── index.ts ├── types │ ├── codes.ts │ ├── pkl.ts │ ├── incoming.ts │ └── outgoing.ts └── evaluator │ ├── module_source.ts │ ├── project.ts │ ├── evaluator_exec.ts │ ├── reader.ts │ ├── evaluator_options.ts │ ├── decoder.ts │ ├── evaluator.ts │ └── evaluator_manager.ts ├── .eslintrc.js ├── package.json ├── README.md └── LICENSE.txt /VERSION.txt: -------------------------------------------------------------------------------- 1 | 0.0.1 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pkl linguist-language=Groovy 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | codegen/snippet-tests/output/* 2 | -------------------------------------------------------------------------------- /codegen/src/tests/gatherer-fixtures/types3.pkl: -------------------------------------------------------------------------------- 1 | class Bike 2 | -------------------------------------------------------------------------------- /e2e/only_primitives/schema.pkl: -------------------------------------------------------------------------------- 1 | addr: String 2 | port: Int16 3 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/05-withPair.pkl: -------------------------------------------------------------------------------- 1 | x: Pair 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.pkl] 2 | indent_style = space 3 | indent_size = 2 4 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/07-literalTypes.pkl: -------------------------------------------------------------------------------- 1 | x: "one" 2 | y: "two" 3 | -------------------------------------------------------------------------------- /examples/express-server/ConfigSchema.pkl: -------------------------------------------------------------------------------- 1 | address: String 2 | port: Int16 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .build 4 | .out* 5 | /generator-settings.pkl 6 | dist 7 | -------------------------------------------------------------------------------- /e2e/only_primitives/missingRequired.pkl: -------------------------------------------------------------------------------- 1 | amends "schema.pkl" 2 | 3 | addr = "localhost" 4 | -------------------------------------------------------------------------------- /pkl-gen-typescript/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pkl_typescript_generator_settings.pkl"; 2 | -------------------------------------------------------------------------------- /codegen/src/tests/gatherer-fixtures/types4.pkl: -------------------------------------------------------------------------------- 1 | class Foo { 2 | bar: Listing 3 | } 4 | -------------------------------------------------------------------------------- /e2e/only_primitives/correct.pkl: -------------------------------------------------------------------------------- 1 | amends "schema.pkl" 2 | 3 | addr = "localhost" 4 | port = 3000 5 | -------------------------------------------------------------------------------- /e2e/only_primitives/wrongType.pkl: -------------------------------------------------------------------------------- 1 | amends "schema.pkl" 2 | 3 | addr = "localhost" 4 | port = "3000" 5 | -------------------------------------------------------------------------------- /codegen/src/PklProject.deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "resolvedDependencies": {} 4 | } 5 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/10-namedMod.pkl: -------------------------------------------------------------------------------- 1 | module n10.pkl.typescript.tests.namedModule 2 | 3 | x: String 4 | -------------------------------------------------------------------------------- /e2e/only_primitives/outOfBounds.pkl: -------------------------------------------------------------------------------- 1 | amends "schema.pkl" 2 | 3 | addr = "localhost" 4 | port = 3000000000000 5 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/support/moduleWithClass.pkl: -------------------------------------------------------------------------------- 1 | class ExampleClass { 2 | x: String 3 | y: Int 4 | } 5 | -------------------------------------------------------------------------------- /examples/basic-intro/config.pkl: -------------------------------------------------------------------------------- 1 | firstName: String = "Phillip" 2 | lastName: String = "Pklton" 3 | age: Int8 = 76 4 | -------------------------------------------------------------------------------- /examples/express-server/config.dev.pkl: -------------------------------------------------------------------------------- 1 | amends "ConfigSchema.pkl" 2 | 3 | address = "localhost" 4 | port = 3003 5 | -------------------------------------------------------------------------------- /examples/express-server/config.prod.pkl: -------------------------------------------------------------------------------- 1 | amends "ConfigSchema.pkl" 2 | 3 | address = "example.com" 4 | port = 443 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | schedule: 5 | interval: "weekly" 6 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/11-withImport.pkl: -------------------------------------------------------------------------------- 1 | import "support/moduleWithClass.pkl" 2 | 3 | value: moduleWithClass.ExampleClass 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "trailingComma": "all", 5 | "useTabs": false, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /codegen/snippet-tests/generator-settings.pkl: -------------------------------------------------------------------------------- 1 | amends "../src/GeneratorSettings.pkl" 2 | 3 | outputDirectory = "codegen/snippet-tests/output" 4 | -------------------------------------------------------------------------------- /bin/pkl-gen-typescript.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import pklGenTypescript from "../pkl-gen-typescript/main"; 3 | 4 | pklGenTypescript(process.argv.slice(2)); 5 | -------------------------------------------------------------------------------- /codegen/src/tests/gatherer-fixtures/types2.pkl: -------------------------------------------------------------------------------- 1 | import "types3.pkl" 2 | 3 | class Person { 4 | bikes: Listing 5 | otherbikes: Listing 6 | } 7 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/03-nullables.pkl: -------------------------------------------------------------------------------- 1 | nullableString: String? 2 | nullableInt: Int? 3 | 4 | nullableListingOfStrings: Listing? 5 | listingOfNullableStrings: Listing 6 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/01-primitiveTypes.pkl: -------------------------------------------------------------------------------- 1 | str: String 2 | int: Int 3 | int8: Int8 4 | uint: UInt 5 | float: Float 6 | bool: Boolean 7 | nullType: Null 8 | anyType: Any 9 | nothingType: nothing 10 | -------------------------------------------------------------------------------- /tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["dist", "node_modules", "examples", "e2e", "codegen"], 4 | "compilerOptions": { 5 | "declaration": true, 6 | "outDir": "./dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/09-customTypes.pkl: -------------------------------------------------------------------------------- 1 | varAny: Any 2 | varDymaic: Dynamic 3 | varDataSizeUnit: DataSizeUnit 4 | varDataSize: DataSize 5 | varDuration: Duration 6 | varIntSeq: IntSeq 7 | varRegex: Regex 8 | varPair: Pair 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | moduleNameMapper: { 6 | "^@pkl-community/pkl-typescript$": "/src", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/06-withTypeAlias.pkl: -------------------------------------------------------------------------------- 1 | typealias MyStringAlias = String 2 | 3 | x: MyStringAlias 4 | 5 | class MyClassToBeAliased { 6 | a: String 7 | b: Int 8 | } 9 | 10 | typealias MyAliasedClass = MyClassToBeAliased 11 | 12 | y: MyAliasedClass 13 | -------------------------------------------------------------------------------- /examples/basic-intro/index.ts: -------------------------------------------------------------------------------- 1 | import { type Config, loadFromPath } from "./generated/config.pkl"; 2 | 3 | const main = async () => { 4 | const config: Config = await loadFromPath("config.pkl"); 5 | console.log( 6 | `Hello, ${config.firstName} ${config.lastName}! I hear you are ${config.age} years old.` 7 | ); 8 | }; 9 | 10 | main(); 11 | -------------------------------------------------------------------------------- /codegen/src/internal/Gen.pkl: -------------------------------------------------------------------------------- 1 | abstract module pkl.typescript.internal.Gen 2 | 3 | import "TypescriptMapping.pkl" 4 | 5 | /// The generated contents for this particular mapping. 6 | mapping: TypescriptMapping 7 | 8 | /// All mappings 9 | mappings: List 10 | 11 | /// The TypeScript contents 12 | contents: String 13 | 14 | typescriptModule: String? 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "baseUrl": ".", 7 | "outDir": "dist", 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "paths": { 12 | "@pkl-community/pkl-typescript": ["./src"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/basic-intro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-intro", 3 | "version": "0.0.1", 4 | "module": "./index.ts", 5 | "dependencies": { 6 | "pkl-typescript": "file:../..", 7 | "tsx": "^4.7.1", 8 | "typescript": "^5.3.3" 9 | }, 10 | "scripts": { 11 | "gen-config": "pkl-gen-typescript ./config.pkl -o ./generated", 12 | "start": "tsx ./index.ts" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /codegen/snippet-tests/test.pkl: -------------------------------------------------------------------------------- 1 | amends "pkl:test" 2 | 3 | import "pkl:reflect" 4 | import "../src/Generator.pkl" 5 | 6 | facts { 7 | for (_, mod in import*("input/*.pkl")) { 8 | [reflect.Module(mod).name] { 9 | for (name, output in new Generator { moduleToGenerate = mod }.output.files!!) { 10 | read?("output/\(name)")?.text == output.text 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/02-collections.pkl: -------------------------------------------------------------------------------- 1 | listingAny: Listing 2 | listingString: Listing 3 | mappingAny: Mapping 4 | mappingStringString: Mapping 5 | mappingStringInt: Mapping 6 | 7 | listAny: List 8 | listString: List 9 | mapAny: Map 10 | mapStringString: Map 11 | mapStringInt: Map 12 | setAny: Set 13 | setString: Set 14 | setInt: Set 15 | -------------------------------------------------------------------------------- /examples/express-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-server", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "@types/express": "^4.17.21", 6 | "express": "^4.19.2", 7 | "pkl-typescript": "file:../..", 8 | "tsx": "^4.7.1", 9 | "typescript": "^5.3.3" 10 | }, 11 | "scripts": { 12 | "gen-config": "pkl-gen-typescript ./ConfigSchema.pkl -o ./generated", 13 | "start": "tsx ./index.ts" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pkl-gen-typescript/generated/README.md: -------------------------------------------------------------------------------- 1 | # GeneratorSettings.pkl.ts 2 | 3 | The file `pkl_typescript_generatorsettings.pkl.ts` in this directory is itself generated by `pkl-gen-typescript`, based on `codegen/src/GeneratorSettings.pkl`. 4 | 5 | To regenerate it, when `GeneratorSettings.pkl` changes or when the behaviour of codegen changes, run: 6 | 7 | ``` 8 | # From the root of the repo 9 | npx pkl-gen-typescript ./codegen/src/GeneratorSettings.pkl -o ./pkl-gen-typescript/generated 10 | ``` 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {PreconfiguredOptions, EvaluatorOptions} from "./evaluator/evaluator_options"; 2 | export {FileSource, TextSource, UriSource, ModuleSource} from "./evaluator/module_source"; 3 | 4 | export { 5 | newEvaluator, newEvaluatorWithCommand, newProjectEvaluator, newProjectEvaluatorWithCommand 6 | } from "./evaluator/evaluator_exec"; 7 | export {newEvaluatorManager, newEvaluatorManagerWithCommand} from "./evaluator/evaluator_manager"; 8 | export type {Evaluator} from "./evaluator/evaluator"; 9 | export * from "./types/pkl" 10 | -------------------------------------------------------------------------------- /codegen/src/internal/TypeAliasGen.pkl: -------------------------------------------------------------------------------- 1 | module pkl.typescript.internal.TypeAliasGen 2 | 3 | extends "Gen.pkl" 4 | 5 | import "typegen.pkl" 6 | import "Type.pkl" 7 | import "pkl:reflect" 8 | 9 | typealiaz: reflect.TypeAlias = mapping.source as reflect.TypeAlias 10 | 11 | type: Type = typegen.generateType(typealiaz.referent, typealiaz, mappings) 12 | 13 | contents = new Listing { 14 | "// Ref: Pkl type `\(typealiaz.enclosingDeclaration.name).\(typealiaz.name)`." 15 | 16 | "type \(module.mapping.name) = \(type.render(module.typescriptModule))" 17 | }.join("\n") 18 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/12-moduleNameOverride.pkl: -------------------------------------------------------------------------------- 1 | @typescript.Name { value = "N12TypescriptGeneratedInterface" } 2 | module n12.pkl.typescript.tests.moduleNameOverride 3 | 4 | import "../../src/typescript.pkl" 5 | 6 | @typescript.Name { value = "MyCustomClassName" } 7 | class CustomClassImpl { 8 | name: String 9 | } 10 | 11 | @typescript.Name { value = "classTypeProperty" } 12 | x: CustomClassImpl 13 | 14 | @typescript.Name { value = "ThisTypeAlias" } 15 | typealias AliasName = "x"|"y"|CustomClassImpl 16 | 17 | @typescript.Name { value = "typeAliasProperty" } 18 | y: AliasName 19 | -------------------------------------------------------------------------------- /codegen/src/GeneratorSettings.pkl: -------------------------------------------------------------------------------- 1 | // Settings used to configure code generation. 2 | module pkl.typescript.GeneratorSettings 3 | 4 | import "pkl:reflect" 5 | import "typescript.pkl" 6 | 7 | /// The output path to write generated files into. 8 | /// 9 | /// Defaults to `.out`. Relative paths are resolved against the enclosing directory. 10 | outputDirectory: String? 11 | 12 | /// If true, evaluates the Pkl modules to check that they could be generated, 13 | /// and prints the filenames that would be created, but skips writing any files. 14 | dryRun: Boolean? 15 | 16 | /// The Generator.pkl script to use for code generation. 17 | /// 18 | /// This is an internal setting that's meant for development purposes. 19 | generatorScriptPath: String? 20 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/08-withUnion.pkl: -------------------------------------------------------------------------------- 1 | // Primitive unions 2 | 3 | x: "one"|"another" 4 | y: String|Int 5 | 6 | // Class union 7 | 8 | class X1 { 9 | firstName: String 10 | } 11 | 12 | class X2 { 13 | lastName: String 14 | } 15 | 16 | x1or2: X1|X2 17 | 18 | // Typealias union 19 | 20 | typealias PersonName = String(!isEmpty) 21 | typealias PersonAge = Int(isBetween(0, 120)) 22 | typealias PersonProperty = PersonName|PersonAge 23 | personFact: PersonProperty 24 | 25 | // Discriminated union 26 | 27 | class Apple { 28 | name: "apple" 29 | sweetness: Int 30 | } 31 | 32 | class Orange { 33 | name: "orange" 34 | tartness: Int 35 | } 36 | 37 | typealias Fruit = Apple|Orange 38 | aFruit: Apple|Orange 39 | anotherFruit: Fruit 40 | -------------------------------------------------------------------------------- /examples/express-server/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { loadFromPath } from "./generated/config_schema.pkl"; 3 | 4 | const app = express(); 5 | 6 | app.get("/", (_, res) => { 7 | res.send("Hello from a pkl-configured app!"); 8 | }); 9 | 10 | // The config file to evaluate can be dynamically chosen based on the value of NODE_ENV 11 | const configFile = `config.${process.env.NODE_ENV ?? "dev"}.pkl`; 12 | 13 | // Use pkl-typescript to load and evaluate the selected Pkl file 14 | loadFromPath(configFile).then((config) => { 15 | console.log("Loaded config values from Pkl:", JSON.stringify(config)); 16 | 17 | // `config` is a typed object, of the schema given in ConfigSchema.pkl 18 | app.listen(config.port, config.address, () => { 19 | console.log("Server started"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/types/codes.ts: -------------------------------------------------------------------------------- 1 | export const 2 | codeNewEvaluator = 0x20 as const, 3 | codeNewEvaluatorResponse = 0x21 as const, 4 | codeCloseEvaluator = 0x22 as const, 5 | codeEvaluate = 0x23 as const, 6 | codeEvaluateResponse = 0x24 as const, 7 | codeEvaluateLog = 0x25 as const, 8 | codeEvaluateRead = 0x26 as const, 9 | codeEvaluateReadResponse = 0x27 as const, 10 | codeEvaluateReadModule = 0x28 as const, 11 | codeEvaluateReadModuleResponse = 0x29 as const, 12 | codeListResourcesRequest = 0x2a as const, 13 | codeListResourcesResponse = 0x2b as const, 14 | codeListModulesRequest = 0x2c as const, 15 | codeListModulesResponse = 0x2d as const; 16 | 17 | export type OutgoingCode = 18 | typeof codeNewEvaluator 19 | | typeof codeCloseEvaluator 20 | | typeof codeEvaluate 21 | | typeof codeEvaluateReadResponse 22 | | typeof codeEvaluateReadModuleResponse 23 | | typeof codeListResourcesResponse 24 | | typeof codeListModulesResponse; 25 | -------------------------------------------------------------------------------- /examples/basic-intro/generated/config.pkl.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by `pkl-typescript` from Pkl module `config`. 2 | // DO NOT EDIT. 3 | import * as pklTypescript from "@pkl-community/pkl-typescript" 4 | 5 | // Ref: Module root. 6 | export interface Config { 7 | firstName: string 8 | 9 | lastName: string 10 | 11 | age: number 12 | } 13 | 14 | // LoadFromPath loads the pkl module at the given path and evaluates it into a Config 15 | export const loadFromPath = async (path: string): Promise => { 16 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 17 | try { 18 | const result = await load(evaluator, pklTypescript.FileSource(path)); 19 | return result 20 | } finally { 21 | evaluator.close() 22 | } 23 | }; 24 | 25 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 26 | evaluator.evaluateModule(source) as Promise; 27 | -------------------------------------------------------------------------------- /codegen/src/PklProject: -------------------------------------------------------------------------------- 1 | amends "pkl:Project" 2 | 3 | local repo = "github.com/pkl-community/pkl-typescript" 4 | 5 | package { 6 | name = "pkl.typescript" 7 | baseUri = "package://pkg.pkl-lang.org/gh/pkl-community/pkl-typescript/codegen/src" 8 | packageZipUrl = "https://\(repo)/releases/download/\(name)@\(version)/\(name)@\(version).zip" 9 | version = read("../../VERSION.txt").text.trim() 10 | authors { 11 | "The Pkl Community " 12 | "Jack Kleeman " 13 | "Jason Gwartz " 14 | } 15 | sourceCodeUrlScheme = "https://\(repo)/tree/v\(version)/codegen/src%{path}#L%{line}-L%{endLine}" 16 | sourceCode = "https://\(repo)" 17 | description = "Pkl bindings for the TypeScript programming language" 18 | license = "Apache-2.0" 19 | exclude { 20 | "tests" 21 | "tests/**" 22 | } 23 | } 24 | 25 | tests { 26 | for (key, _ in import*("tests/*.pkl")) { 27 | key 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /codegen/src/tests/gatherer-fixtures/types.pkl: -------------------------------------------------------------------------------- 1 | class Bike { 2 | isFixie: Boolean 3 | } 4 | 5 | abstract class Being { 6 | isAlive: Boolean 7 | } 8 | 9 | /// A Person! 10 | open class Person extends Being { 11 | bike: Bike 12 | 13 | /// The person's first name 14 | firstName: UInt16? 15 | 16 | /// The person's last name 17 | lastName: Mapping 18 | } 19 | 20 | typealias BugKind = "butterfly" | "beetle\"" | "beetle one" | "beetle_one" 21 | 22 | typealias SymbolKind = "*" | "beetle\"" | "!!!" | "__" 23 | 24 | class Bug { 25 | /// The owner of this bug. 26 | owner: Person? 27 | 28 | secondOwner: Person 29 | 30 | /// The age of this bug 31 | age: Int? 32 | 33 | /// How long the bug holds its breath for 34 | holdsBreathFor: Duration 35 | 36 | size: DataSize 37 | 38 | kind: BugKind 39 | 40 | symbol: SymbolKind 41 | } 42 | 43 | class Cyclic { 44 | a: String 45 | 46 | b: Int 47 | 48 | myself: Cyclic 49 | } 50 | -------------------------------------------------------------------------------- /examples/express-server/generated/config_schema.pkl.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by `pkl-typescript` from Pkl module `ConfigSchema`. 2 | // DO NOT EDIT. 3 | import * as pklTypescript from "@pkl-community/pkl-typescript" 4 | 5 | // Ref: Module root. 6 | export interface ConfigSchema { 7 | address: string 8 | 9 | port: number 10 | } 11 | 12 | // LoadFromPath loads the pkl module at the given path and evaluates it into a ConfigSchema 13 | export const loadFromPath = async (path: string): Promise => { 14 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 15 | try { 16 | const result = await load(evaluator, pklTypescript.FileSource(path)); 17 | return result 18 | } finally { 19 | evaluator.close() 20 | } 21 | }; 22 | 23 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 24 | evaluator.evaluateModule(source) as Promise; 25 | -------------------------------------------------------------------------------- /codegen/snippet-tests/output/05_with_pair.pkl.ts: -------------------------------------------------------------------------------- 1 | /* This file was generated by `pkl-typescript` from Pkl module `05-withPair`. */ 2 | /* DO NOT EDIT! */ 3 | /* istanbul ignore file */ 4 | /* eslint-disable */ 5 | import * as pklTypescript from "@pkl-community/pkl-typescript" 6 | 7 | // Ref: Module root. 8 | export interface N05WithPair { 9 | x: pklTypescript.Pair 10 | } 11 | 12 | // LoadFromPath loads the pkl module at the given path and evaluates it into a N05WithPair 13 | export const loadFromPath = async (path: string): Promise => { 14 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 15 | try { 16 | const result = await load(evaluator, pklTypescript.FileSource(path)); 17 | return result 18 | } finally { 19 | evaluator.close() 20 | } 21 | }; 22 | 23 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 24 | evaluator.evaluateModule(source) as Promise; 25 | -------------------------------------------------------------------------------- /codegen/snippet-tests/output/07_literal_types.pkl.ts: -------------------------------------------------------------------------------- 1 | /* This file was generated by `pkl-typescript` from Pkl module `07-literalTypes`. */ 2 | /* DO NOT EDIT! */ 3 | /* istanbul ignore file */ 4 | /* eslint-disable */ 5 | import * as pklTypescript from "@pkl-community/pkl-typescript" 6 | 7 | // Ref: Module root. 8 | export interface N07LiteralTypes { 9 | x: "one" 10 | 11 | y: "two" 12 | } 13 | 14 | // LoadFromPath loads the pkl module at the given path and evaluates it into a N07LiteralTypes 15 | export const loadFromPath = async (path: string): Promise => { 16 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 17 | try { 18 | const result = await load(evaluator, pklTypescript.FileSource(path)); 19 | return result 20 | } finally { 21 | evaluator.close() 22 | } 23 | }; 24 | 25 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 26 | evaluator.evaluateModule(source) as Promise; 27 | -------------------------------------------------------------------------------- /codegen/snippet-tests/output/n10_pkl_typescript_tests_named_module.pkl.ts: -------------------------------------------------------------------------------- 1 | /* This file was generated by `pkl-typescript` from Pkl module `n10.pkl.typescript.tests.namedModule`. */ 2 | /* DO NOT EDIT! */ 3 | /* istanbul ignore file */ 4 | /* eslint-disable */ 5 | import * as pklTypescript from "@pkl-community/pkl-typescript" 6 | 7 | // Ref: Module root. 8 | export interface NamedModule { 9 | x: string 10 | } 11 | 12 | // LoadFromPath loads the pkl module at the given path and evaluates it into a NamedModule 13 | export const loadFromPath = async (path: string): Promise => { 14 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 15 | try { 16 | const result = await load(evaluator, pklTypescript.FileSource(path)); 17 | return result 18 | } finally { 19 | evaluator.close() 20 | } 21 | }; 22 | 23 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 24 | evaluator.evaluateModule(source) as Promise; 25 | -------------------------------------------------------------------------------- /codegen/src/typescript.pkl: -------------------------------------------------------------------------------- 1 | @Module { name = "pkl_gen_typescript" } 2 | module pkl.typescript.typescript 3 | 4 | import "internal/utils.pkl" 5 | 6 | /// A name in TypeScript/JavaScript; i.e., an identifier. 7 | typealias TypeScriptName = String(utils.isValidTypescriptName) 8 | 9 | /// Metadata for the corresponding TypeScript module for the annotated Pkl module(s). 10 | /// 11 | /// This flag 12 | /// 13 | /// Example: 14 | /// 15 | /// ``` 16 | /// @typescript.Module { name = "MyLibrary" } 17 | /// module myteam.myproj.MyLibrary 18 | /// 19 | /// import "package://pkg.pkl-lang.org/pkl-typescript/pkl.typescript@#/typescript.pkl" 20 | /// ``` 21 | class Module extends Annotation { 22 | /// The full name of the TypeScript module 23 | name: TypeScriptName 24 | } 25 | 26 | /// The name for the annotated member when generated to TypeScript. 27 | /// 28 | /// This may be used to annotate modules, classes, typealiases, and properties. 29 | /// 30 | /// Examples: 31 | /// 32 | /// ``` 33 | /// @typescript.Name { value = "ThePerson" } 34 | /// class Person 35 | /// ``` 36 | class Name extends Annotation { 37 | value: TypeScriptName 38 | } 39 | -------------------------------------------------------------------------------- /codegen/src/tests/gatherer.test.pkl: -------------------------------------------------------------------------------- 1 | // As with gatherer.pkl, this is 100% copy-pasted from pkl-go and pkl-swift. 2 | amends "pkl:test" 3 | 4 | import "pkl:reflect" 5 | import "gatherer-fixtures/types.pkl" 6 | import "gatherer-fixtures/types2.pkl" 7 | import "gatherer-fixtures/types4.pkl" 8 | import "../internal/gatherer.pkl" 9 | 10 | // it's important that these classes are defined in another module because they gather the type 11 | // declarations of their enclosing module. 12 | facts { 13 | ["gather type declarations"] { 14 | gatherer.gatherTypeDeclarations(reflect.Class(types.Bug), List()).map((c) -> c.name) 15 | == List("Bug", "Person", "Bike", "ModuleClass", "Being", "Cyclic", "BugKind", "SymbolKind") 16 | } 17 | ["gather type declarations - listing arguments"] { 18 | gatherer.gatherTypeDeclarations(reflect.Class(types2.Person), List()).map((c) -> c.name) 19 | == List("Person", "Bike", "ModuleClass", "ModuleClass") 20 | } 21 | ["gather type declarations - type params with unions"] { 22 | gatherer.gatherTypeDeclarations(reflect.Class(types4.Foo), List()).map((c) -> c.name) 23 | == List("Foo", "ModuleClass") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /codegen/snippet-tests/output/03_nullables.pkl.ts: -------------------------------------------------------------------------------- 1 | /* This file was generated by `pkl-typescript` from Pkl module `03-nullables`. */ 2 | /* DO NOT EDIT! */ 3 | /* istanbul ignore file */ 4 | /* eslint-disable */ 5 | import * as pklTypescript from "@pkl-community/pkl-typescript" 6 | 7 | // Ref: Module root. 8 | export interface N03Nullables { 9 | nullableString: string|null 10 | 11 | nullableInt: number|null 12 | 13 | nullableListingOfStrings: Array|null 14 | 15 | listingOfNullableStrings: Array 16 | } 17 | 18 | // LoadFromPath loads the pkl module at the given path and evaluates it into a N03Nullables 19 | export const loadFromPath = async (path: string): Promise => { 20 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 21 | try { 22 | const result = await load(evaluator, pklTypescript.FileSource(path)); 23 | return result 24 | } finally { 25 | evaluator.close() 26 | } 27 | }; 28 | 29 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 30 | evaluator.evaluateModule(source) as Promise; 31 | -------------------------------------------------------------------------------- /codegen/snippet-tests/input/04-withClass.pkl: -------------------------------------------------------------------------------- 1 | class MyCustomClass { 2 | x: String 3 | y: Int 4 | 5 | // Function type properties should not be included in codegen 6 | funcType: (String) -> (String) 7 | } 8 | 9 | value: MyCustomClass 10 | 11 | abstract class MyAbstractClass { 12 | someString: String 13 | overrideableStringType: String 14 | overridableListing1: Listing 15 | overridableListing2: Listing 16 | overridableMap1: Map 17 | overridableMap2: Map 18 | overridableUnion1: Int|String|List 19 | overridableUnion2: Int|String 20 | } 21 | 22 | class MyConcreteClass extends MyAbstractClass { 23 | anotherString: String 24 | overrideableStringType: "string literal type" 25 | overridableListing1: Listing 26 | overridableListing2: Listing 27 | overridableMap1: Map 28 | overridableMap2: Map 29 | overridableUnion1: String|Int 30 | overridableUnion2: String 31 | } 32 | 33 | open class MyOpenClass { 34 | someString: String 35 | } 36 | 37 | class MySubclassOfOpen extends MyOpenClass { 38 | someInt: Int 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | concurrency: publish-single-concurrency 14 | 15 | # Based on: 16 | # https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 20.x 22 | cache: npm 23 | registry-url: "https://registry.npmjs.org" 24 | 25 | - run: npm ci 26 | 27 | - name: Increment npm package version number 28 | run: | 29 | git config --global user.name "${{ github.actor }}" 30 | git config --global user.email "${{ github.actor }}@users.noreply.github.com" 31 | npm version patch -m "Version bump: v%s" 32 | git push 33 | 34 | - run: npm run build 35 | 36 | # TODO: publish publicly 37 | # - run: npm publish --access public 38 | - run: npm publish 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | -------------------------------------------------------------------------------- /codegen/snippet-tests/output/11_with_import.pkl.ts: -------------------------------------------------------------------------------- 1 | /* This file was generated by `pkl-typescript` from Pkl module `11-withImport`. */ 2 | /* DO NOT EDIT! */ 3 | /* istanbul ignore file */ 4 | /* eslint-disable */ 5 | import * as pklTypescript from "@pkl-community/pkl-typescript" 6 | 7 | // Ref: Module root. 8 | export interface N11WithImport { 9 | value: ExampleClass 10 | } 11 | 12 | // Ref: Pkl class `moduleWithClass.ExampleClass`. 13 | export interface ExampleClass { 14 | x: string 15 | 16 | y: number 17 | } 18 | 19 | // Ref: Module root. 20 | export interface ModuleWithClass { 21 | } 22 | 23 | // LoadFromPath loads the pkl module at the given path and evaluates it into a N11WithImport 24 | export const loadFromPath = async (path: string): Promise => { 25 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 26 | try { 27 | const result = await load(evaluator, pklTypescript.FileSource(path)); 28 | return result 29 | } finally { 30 | evaluator.close() 31 | } 32 | }; 33 | 34 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 35 | evaluator.evaluateModule(source) as Promise; 36 | -------------------------------------------------------------------------------- /codegen/snippet-tests/output/01_primitive_types.pkl.ts: -------------------------------------------------------------------------------- 1 | /* This file was generated by `pkl-typescript` from Pkl module `01-primitiveTypes`. */ 2 | /* DO NOT EDIT! */ 3 | /* istanbul ignore file */ 4 | /* eslint-disable */ 5 | import * as pklTypescript from "@pkl-community/pkl-typescript" 6 | 7 | // Ref: Module root. 8 | export interface N01PrimitiveTypes { 9 | str: string 10 | 11 | int: number 12 | 13 | int8: number 14 | 15 | uint: number 16 | 17 | float: number 18 | 19 | bool: boolean 20 | 21 | nullType: null 22 | 23 | anyType: pklTypescript.Any 24 | 25 | nothingType: never 26 | } 27 | 28 | // LoadFromPath loads the pkl module at the given path and evaluates it into a N01PrimitiveTypes 29 | export const loadFromPath = async (path: string): Promise => { 30 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 31 | try { 32 | const result = await load(evaluator, pklTypescript.FileSource(path)); 33 | return result 34 | } finally { 35 | evaluator.close() 36 | } 37 | }; 38 | 39 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 40 | evaluator.evaluateModule(source) as Promise; 41 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: read 10 | 11 | jobs: 12 | eslint: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | cache: "npm" 20 | 21 | - run: npm ci 22 | 23 | - name: Run eslint on changed files 24 | uses: tj-actions/eslint-changed-files@v20 25 | with: 26 | reporter: github-check 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | prettier: 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-node@v4 35 | with: 36 | cache: "npm" 37 | 38 | - run: npm ci 39 | 40 | - name: Get changed files 41 | id: changed-files 42 | uses: tj-actions/changed-files@v38 43 | 44 | - run: npx prettier --check ${{ steps.changed-files.outputs.all_changed_files }} 45 | 46 | actionlint: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | - name: Check workflow files 51 | uses: docker://rhysd/actionlint:latest 52 | with: 53 | args: -color 54 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | parser: "@typescript-eslint/parser", 4 | plugins: ["@typescript-eslint", "simple-import-sort"], 5 | 6 | parserOptions: { 7 | project: true, 8 | }, 9 | 10 | extends: [ 11 | "plugin:@typescript-eslint/strict-type-checked", 12 | "plugin:@typescript-eslint/stylistic-type-checked", 13 | "prettier", 14 | ], 15 | 16 | rules: { 17 | "@typescript-eslint/consistent-type-imports": "error", 18 | "simple-import-sort/imports": "error", 19 | camelcase: ["error", { properties: "always" }], 20 | eqeqeq: ["error", "always"], 21 | "prefer-const": "error", 22 | "@typescript-eslint/no-throw-literal": "error", 23 | "@typescript-eslint/switch-exhaustiveness-check": "error", 24 | // Specifically allow "_" as a placeholder argument name 25 | "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], 26 | }, 27 | 28 | overrides: [ 29 | { 30 | files: ["./*.js", "./*.mjs"], // config files 31 | env: { node: true }, 32 | extends: ["plugin:@typescript-eslint/disable-type-checked"], 33 | rules: { 34 | "@typescript-eslint/no-var-requires": "off", 35 | }, 36 | }, 37 | { 38 | files: ["**/*.test.ts"], 39 | plugins: ["jest"], 40 | }, 41 | ], 42 | }; 43 | -------------------------------------------------------------------------------- /codegen/snippet-tests/output/09_custom_types.pkl.ts: -------------------------------------------------------------------------------- 1 | /* This file was generated by `pkl-typescript` from Pkl module `09-customTypes`. */ 2 | /* DO NOT EDIT! */ 3 | /* istanbul ignore file */ 4 | /* eslint-disable */ 5 | import * as pklTypescript from "@pkl-community/pkl-typescript" 6 | 7 | // Ref: Module root. 8 | export interface N09CustomTypes { 9 | varAny: pklTypescript.Any 10 | 11 | varDymaic: pklTypescript.Dynamic 12 | 13 | varDataSizeUnit: pklTypescript.DataSizeUnit 14 | 15 | varDataSize: pklTypescript.DataSize 16 | 17 | varDuration: pklTypescript.Duration 18 | 19 | varIntSeq: pklTypescript.IntSeq 20 | 21 | varRegex: pklTypescript.Regex 22 | 23 | varPair: pklTypescript.Pair 24 | } 25 | 26 | // LoadFromPath loads the pkl module at the given path and evaluates it into a N09CustomTypes 27 | export const loadFromPath = async (path: string): Promise => { 28 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 29 | try { 30 | const result = await load(evaluator, pklTypescript.FileSource(path)); 31 | return result 32 | } finally { 33 | evaluator.close() 34 | } 35 | }; 36 | 37 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 38 | evaluator.evaluateModule(source) as Promise; 39 | -------------------------------------------------------------------------------- /e2e/only_primitives/primitives.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@jest/globals"; 2 | 3 | import pklGenTypescript from "../../pkl-gen-typescript/main"; 4 | import { join } from "path"; 5 | 6 | describe("E2E of config with only primitive values", () => { 7 | it("can generate TypeScript sources and load valid values", async () => { 8 | await pklGenTypescript([join(__dirname, "schema.pkl")]); 9 | const configPkl = await import(join(__dirname, "../../.out/schema.pkl.ts")); 10 | const config = await configPkl.loadFromPath(join(__dirname, "correct.pkl")); 11 | expect(config).toStrictEqual({ 12 | addr: "localhost", 13 | port: 3000, 14 | }); 15 | }); 16 | 17 | it.each([ 18 | ["missing a required property", "missingRequired.pkl"], 19 | ["property is of the wrong type", "wrongType.pkl"], 20 | ["property fails validation constraint", "outOfBounds.pkl"], 21 | ])( 22 | "can generate TypeScript sources but error on evaluating invalid values: %s", 23 | async (_, fileBase) => { 24 | await pklGenTypescript([join(__dirname, "schema.pkl")]); 25 | const configPkl = await import( 26 | join(__dirname, "../../.out/schema.pkl.ts") 27 | ); 28 | await expect( 29 | configPkl.loadFromPath(join(__dirname, `${fileBase}.pkl`)) 30 | ).rejects.toThrowError(); 31 | } 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /codegen/snippet-tests/output/06_with_type_alias.pkl.ts: -------------------------------------------------------------------------------- 1 | /* This file was generated by `pkl-typescript` from Pkl module `06-withTypeAlias`. */ 2 | /* DO NOT EDIT! */ 3 | /* istanbul ignore file */ 4 | /* eslint-disable */ 5 | import * as pklTypescript from "@pkl-community/pkl-typescript" 6 | 7 | // Ref: Module root. 8 | export interface N06WithTypeAlias { 9 | x: MyStringAlias 10 | 11 | y: MyAliasedClass 12 | } 13 | 14 | // Ref: Pkl class `06-withTypeAlias.MyClassToBeAliased`. 15 | export interface MyClassToBeAliased { 16 | a: string 17 | 18 | b: number 19 | } 20 | 21 | // Ref: Pkl type `06-withTypeAlias.MyStringAlias`. 22 | type MyStringAlias = string 23 | 24 | // Ref: Pkl type `06-withTypeAlias.MyAliasedClass`. 25 | type MyAliasedClass = MyClassToBeAliased 26 | 27 | // LoadFromPath loads the pkl module at the given path and evaluates it into a N06WithTypeAlias 28 | export const loadFromPath = async (path: string): Promise => { 29 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 30 | try { 31 | const result = await load(evaluator, pklTypescript.FileSource(path)); 32 | return result 33 | } finally { 34 | evaluator.close() 35 | } 36 | }; 37 | 38 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 39 | evaluator.evaluateModule(source) as Promise; 40 | -------------------------------------------------------------------------------- /codegen/src/tests/ClassGen.test.pkl-expected.pcf: -------------------------------------------------------------------------------- 1 | examples { 2 | ["Empty class"] { 3 | """ 4 | // Ref: Pkl class `ClassGen.test.EmptyClass`. 5 | export interface EmptyClass { 6 | } 7 | """ 8 | } 9 | ["One-property class"] { 10 | """ 11 | // Ref: Pkl class `ClassGen.test.OnePropertyClass`. 12 | export interface OnePropertyClass { 13 | name: string 14 | } 15 | """ 16 | } 17 | ["Multi-property class"] { 18 | """ 19 | // Ref: Pkl class `ClassGen.test.MultiPropertyClass`. 20 | export interface MultiPropertyClass { 21 | name: string 22 | 23 | age: number 24 | } 25 | """ 26 | } 27 | ["Class with a listing proprety"] { 28 | """ 29 | // Ref: Pkl class `ClassGen.test.WithListingClass`. 30 | export interface WithListingClass { 31 | name: string 32 | 33 | hobbies: Array 34 | } 35 | """ 36 | } 37 | ["Class with a mapping property"] { 38 | """ 39 | // Ref: Pkl class `ClassGen.test.WithMappingClass`. 40 | export interface WithMappingClass { 41 | name: string 42 | 43 | siblingAges: Map 44 | } 45 | """ 46 | } 47 | ["Class with a nested mapping property"] { 48 | """ 49 | // Ref: Pkl class `ClassGen.test.WithNestedMappingClass`. 50 | export interface WithNestedMappingClass { 51 | name: string 52 | 53 | siblings: Map> 54 | } 55 | """ 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /codegen/snippet-tests/output/n12_pkl_typescript_tests_module_name_override.pkl.ts: -------------------------------------------------------------------------------- 1 | /* This file was generated by `pkl-typescript` from Pkl module `n12.pkl.typescript.tests.moduleNameOverride`. */ 2 | /* DO NOT EDIT! */ 3 | /* istanbul ignore file */ 4 | /* eslint-disable */ 5 | import * as pklTypescript from "@pkl-community/pkl-typescript" 6 | 7 | // Ref: Module root. 8 | export interface N12TypescriptGeneratedInterface { 9 | classTypeProperty: MyCustomClassName 10 | 11 | typeAliasProperty: ThisTypeAlias 12 | } 13 | 14 | // Ref: Pkl class `n12.pkl.typescript.tests.moduleNameOverride.CustomClassImpl`. 15 | export interface MyCustomClassName { 16 | name: string 17 | } 18 | 19 | // Ref: Pkl type `n12.pkl.typescript.tests.moduleNameOverride.AliasName`. 20 | type ThisTypeAlias = "x" | "y" | MyCustomClassName 21 | 22 | // LoadFromPath loads the pkl module at the given path and evaluates it into a N12TypescriptGeneratedInterface 23 | export const loadFromPath = async (path: string): Promise => { 24 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 25 | try { 26 | const result = await load(evaluator, pklTypescript.FileSource(path)); 27 | return result 28 | } finally { 29 | evaluator.close() 30 | } 31 | }; 32 | 33 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 34 | evaluator.evaluateModule(source) as Promise; 35 | -------------------------------------------------------------------------------- /pkl-gen-typescript/generated/pkl_typescript_generator_settings.pkl.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by `pkl-typescript` from Pkl module `pkl.typescript.GeneratorSettings`. 2 | // DO NOT EDIT. 3 | import * as pklTypescript from "@pkl-community/pkl-typescript" 4 | 5 | // Ref: Module root. 6 | export interface GeneratorSettings { 7 | // The output path to write generated files into. 8 | // 9 | // Defaults to `.out`. Relative paths are resolved against the enclosing directory. 10 | outputDirectory: string|null 11 | 12 | // If true, evaluates the Pkl modules to check that they could be generated, 13 | // and prints the filenames that would be created, but skips writing any files. 14 | dryRun: boolean|null 15 | 16 | // The Generator.pkl script to use for code generation. 17 | // 18 | // This is an internal setting that's meant for development purposes. 19 | generatorScriptPath: string|null 20 | } 21 | 22 | // LoadFromPath loads the pkl module at the given path and evaluates it into a GeneratorSettings 23 | export const loadFromPath = async (path: string): Promise => { 24 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 25 | const result = await load(evaluator, pklTypescript.FileSource(path)); 26 | evaluator.close() 27 | return result 28 | }; 29 | 30 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 31 | evaluator.evaluateModule(source) as Promise; 32 | -------------------------------------------------------------------------------- /src/evaluator/module_source.ts: -------------------------------------------------------------------------------- 1 | // ModuleSource represents a source for Pkl evaluation. 2 | import * as path from "node:path"; 3 | 4 | export type ModuleSource = { 5 | // uri is the URL of the resource. 6 | uri: URL, 7 | 8 | // Contents is the text contents of the resource, if any. 9 | // 10 | // If Contents is not provided, it gets resolved by Pkl during evaluation time. 11 | // If the scheme of the Uri matches a ModuleReader, it will be used to resolve the module. 12 | contents?: string, 13 | } 14 | 15 | // FileSource builds a ModuleSource, treating its arguments as paths on the file system. 16 | // 17 | // If the provided path is not an absolute path, it will be resolved against the current working 18 | // directory. 19 | // 20 | // If multiple path arguments are provided, they are joined as multiple elements of the path. 21 | export function FileSource(...pathElems: string[]): ModuleSource { 22 | const src = path.resolve(path.join(...pathElems)) 23 | return { 24 | uri: new URL(`file://${src}`) 25 | } 26 | } 27 | 28 | const replTextUri = new URL("repl:text") 29 | 30 | // TextSource builds a ModuleSource whose contents are the provided text. 31 | export function TextSource(text: string): ModuleSource { 32 | return { 33 | uri: replTextUri, 34 | contents: text, 35 | } 36 | } 37 | 38 | // UriSource builds a ModuleSource using the input uri. 39 | export function UriSource(uri: string): ModuleSource { 40 | const parsedUri = new URL(uri) 41 | return { 42 | uri: parsedUri, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /codegen/snippet-tests/output/02_collections.pkl.ts: -------------------------------------------------------------------------------- 1 | /* This file was generated by `pkl-typescript` from Pkl module `02-collections`. */ 2 | /* DO NOT EDIT! */ 3 | /* istanbul ignore file */ 4 | /* eslint-disable */ 5 | import * as pklTypescript from "@pkl-community/pkl-typescript" 6 | 7 | // Ref: Module root. 8 | export interface N02Collections { 9 | listingAny: Array 10 | 11 | listingString: Array 12 | 13 | mappingAny: Map 14 | 15 | mappingStringString: Map 16 | 17 | mappingStringInt: Map 18 | 19 | listAny: Array 20 | 21 | listString: Array 22 | 23 | mapAny: Map 24 | 25 | mapStringString: Map 26 | 27 | mapStringInt: Map 28 | 29 | setAny: Set 30 | 31 | setString: Set 32 | 33 | setInt: Set 34 | } 35 | 36 | // LoadFromPath loads the pkl module at the given path and evaluates it into a N02Collections 37 | export const loadFromPath = async (path: string): Promise => { 38 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 39 | try { 40 | const result = await load(evaluator, pklTypescript.FileSource(path)); 41 | return result 42 | } finally { 43 | evaluator.close() 44 | } 45 | }; 46 | 47 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 48 | evaluator.evaluateModule(source) as Promise; 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | pkl-unit: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | pklVersion: ["0.25.0", "0.25.1", "0.25.2", "0.25.3"] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Install pkl 21 | uses: pkl-community/setup-pkl@v0 22 | with: 23 | pkl-version: ${{ matrix.pklVersion }} 24 | 25 | - name: Run tests 26 | run: | 27 | tests=$(find ./codegen/src/ -name '*.test.pkl') 28 | # shellcheck disable=SC2086 29 | pkl test $tests 30 | 31 | pkl-snippet: 32 | runs-on: ubuntu-latest 33 | 34 | strategy: 35 | matrix: 36 | pklVersion: ["0.25.0", "0.25.1", "0.25.2", "0.25.3"] 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - name: Install pkl 42 | uses: pkl-community/setup-pkl@v0 43 | with: 44 | pkl-version: ${{ matrix.pklVersion }} 45 | 46 | - name: Run tests 47 | run: | 48 | pkl test codegen/snippet-tests/test.pkl 49 | 50 | e2e: 51 | runs-on: ubuntu-latest 52 | 53 | strategy: 54 | matrix: 55 | node-version: [18, 20, 21] 56 | pklVersion: ["0.25.0", "0.25.1", "0.25.2", "0.25.3"] 57 | 58 | steps: 59 | - uses: actions/checkout@v4 60 | - uses: actions/setup-node@v4 61 | with: 62 | node-version: ${{ matrix.node-version }} 63 | cache: npm 64 | 65 | - run: npm install 66 | - run: npm run test:e2e 67 | timeout-minutes: 3 68 | -------------------------------------------------------------------------------- /examples/express-server/README.md: -------------------------------------------------------------------------------- 1 | # PklTypescript Example: Express server 2 | 3 | This is an example of a simple TypeScript program that uses Pkl for configuration. 4 | 5 | ## Hand-written code 6 | 7 | A summary of each of the files in this directory, and what they do: 8 | 9 | - `index.ts`: contains the source code of the express app 10 | - `ConfigSchema.pkl`: contains the schema of the configuration (just the properties and their types, but not any values) 11 | - `config.dev.pkl`: amends `ConfigSchema.pkl`, adding the config values that would be used in local development 12 | - `config.prod.pkl`: amends `ConfigSchema.pkl` but with values to be used in production 13 | 14 | ## Generated code 15 | 16 | There is also another directory, `generated`, that contains `.pkl.ts` files that were generated by `pkl-gen-typescript` based on the given schema. 17 | 18 | The `package.json` has a script, titled `gen-config` (but the script name doesn't matter), that executes the following: 19 | 20 | ```bash 21 | pkl-gen-typescript ./ConfigSchema.pkl -o ./generated 22 | ``` 23 | 24 | This uses the `pkl-gen-typescript` CLI, passing in `ConfigSchema.pkl` as the Pkl module to be evaluated, and outputting the generated TypeScript files to the `./generated` directory. 25 | 26 | ## Running the app 27 | 28 | 1. Run `npm install` in this directory to install dependencies (including a local link to the `pkl-typescript` project at the root of this repo) 29 | 1. Run `npm run gen-config` to make sure the generated `.pkl.ts` files in the `./generated` directory are up-to-date 30 | 1. Run `npm run start` to start the local dev server 31 | 32 | Then, in another terminal, run `curl localhost:3003` to see a response from your Pkl-configured Express server. 33 | -------------------------------------------------------------------------------- /src/evaluator/project.ts: -------------------------------------------------------------------------------- 1 | import {PreconfiguredOptions, ProjectDependencies} from "./evaluator_options"; 2 | import {Evaluator} from "./evaluator"; 3 | import {FileSource} from "./module_source"; 4 | import {newEvaluator} from "./evaluator_exec"; 5 | 6 | // Project is the TS representation of pkl.Project. 7 | export type Project = { 8 | projectFileUri: string 9 | package?: ProjectPackage 10 | evaluatorSettings?: ProjectEvaluatorSettings 11 | tests: string[] 12 | 13 | dependencies: ProjectDependencies 14 | } 15 | 16 | // ProjectPackage is the TS representation of pkl.Project#Package. 17 | type ProjectPackage = { 18 | name: string 19 | baseUri: string 20 | version: string 21 | packageZipUrl: string 22 | description: string 23 | authors: string[] 24 | website: string 25 | documentation: string 26 | sourceCode: string 27 | sourceCodeUrlScheme: string 28 | license: string 29 | licenseText: string 30 | issueTracker: string 31 | apiTests: string[] 32 | exclude: string[] 33 | uri: string[] 34 | } 35 | 36 | // ProjectEvaluatorSettings is the representation of pkl.Project#EvaluatorSettings 37 | export type ProjectEvaluatorSettings = { 38 | externalProperties: Record 39 | env: Record 40 | allowedModules: string[] 41 | allowedResources: string[] 42 | noCache?: boolean 43 | modulePath: string[] 44 | moduleCacheDir: string 45 | rootDir: string 46 | } 47 | 48 | // loadProject loads a project definition from the specified path directory. 49 | export async function loadProject(path: string): Promise { 50 | const ev = await newEvaluator(PreconfiguredOptions) 51 | return loadProjectFromEvaluator(ev, path) 52 | } 53 | 54 | export async function loadProjectFromEvaluator(ev: Evaluator, path: string): Promise { 55 | return await ev.evaluateOutputValue(FileSource(path)) as Project 56 | } 57 | -------------------------------------------------------------------------------- /codegen/src/tests/utils.test.pkl: -------------------------------------------------------------------------------- 1 | amends "pkl:test" 2 | 3 | import "../internal/utils.pkl" 4 | 5 | facts { 6 | ["camelCase"] { 7 | utils.camelCase("hello") == "hello" 8 | utils.camelCase("hello space") == "helloSpace" 9 | utils.camelCase("hello-hyphen") == "helloHyphen" 10 | utils.camelCase("hello_underscore") == "helloUnderscore" 11 | utils.camelCase("StartsWithCapital") == "startsWithCapital" 12 | utils.camelCase("Starts upper") == "startsUpper" 13 | } 14 | ["snakeCase"] { 15 | utils.snakeCase("this is a test") == "this_is_a_test" 16 | utils.snakeCase("fromCamelCase") == "from_camel_case" 17 | utils.snakeCase("FromPascalCase") == "from_pascal_case" 18 | utils.snakeCase("Capitalword") == "capitalword" 19 | } 20 | ["normalizeName"] { 21 | utils.normalizeName("foo") == "Foo" 22 | utils.normalizeName("foo foo") == "FooFoo" 23 | utils.normalizeName("1 foo") == "N1Foo" 24 | utils.normalizeName("bar ` $$ 你好 baz") == "Bar你好Baz" 25 | utils.normalizeName("Go111") == "Go111" 26 | utils.normalizeName("snake_case") == "SnakeCase" 27 | } 28 | ["isValidTypescriptName"] { 29 | utils.isValidTypescriptName.apply("hello") == true 30 | utils.isValidTypescriptName.apply("123") == false 31 | utils.isValidTypescriptName.apply("+invalid") == false 32 | module.catch(() -> utils.isValidTypescriptName.apply("await")).startsWith("Name") 33 | module.catch(() -> utils.isValidTypescriptName.apply("as")).startsWith("Name") 34 | } 35 | ["renderImports"] { 36 | utils.renderImports(List( 37 | "fs" 38 | )) == "import * as fs from \"fs\"" 39 | 40 | utils.renderImports(List( 41 | "fs", 42 | "child_process" 43 | )) == """ 44 | import * as fs from "fs" 45 | import * as childProcess from "child_process" 46 | """ 47 | 48 | utils.renderImports(List( 49 | "copy-to-clipboard" 50 | )) == "import * as copyToClipboard from \"copy-to-clipboard\"" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /codegen/snippet-tests/output/08_with_union.pkl.ts: -------------------------------------------------------------------------------- 1 | /* This file was generated by `pkl-typescript` from Pkl module `08-withUnion`. */ 2 | /* DO NOT EDIT! */ 3 | /* istanbul ignore file */ 4 | /* eslint-disable */ 5 | import * as pklTypescript from "@pkl-community/pkl-typescript" 6 | 7 | // Ref: Module root. 8 | export interface N08WithUnion { 9 | x: "one" | "another" 10 | 11 | y: string | number 12 | 13 | x1or2: X1 | X2 14 | 15 | personFact: PersonProperty 16 | 17 | aFruit: Apple | Orange 18 | 19 | anotherFruit: Fruit 20 | } 21 | 22 | // Ref: Pkl class `08-withUnion.X1`. 23 | export interface X1 { 24 | firstName: string 25 | } 26 | 27 | // Ref: Pkl class `08-withUnion.X2`. 28 | export interface X2 { 29 | lastName: string 30 | } 31 | 32 | // Ref: Pkl class `08-withUnion.Apple`. 33 | export interface Apple { 34 | name: "apple" 35 | 36 | sweetness: number 37 | } 38 | 39 | // Ref: Pkl class `08-withUnion.Orange`. 40 | export interface Orange { 41 | name: "orange" 42 | 43 | tartness: number 44 | } 45 | 46 | // Ref: Pkl type `08-withUnion.PersonProperty`. 47 | type PersonProperty = PersonName | PersonAge 48 | 49 | // Ref: Pkl type `08-withUnion.Fruit`. 50 | type Fruit = Apple | Orange 51 | 52 | // Ref: Pkl type `08-withUnion.PersonName`. 53 | type PersonName = string 54 | 55 | // Ref: Pkl type `08-withUnion.PersonAge`. 56 | type PersonAge = number 57 | 58 | // LoadFromPath loads the pkl module at the given path and evaluates it into a N08WithUnion 59 | export const loadFromPath = async (path: string): Promise => { 60 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 61 | try { 62 | const result = await load(evaluator, pklTypescript.FileSource(path)); 63 | return result 64 | } finally { 65 | evaluator.close() 66 | } 67 | }; 68 | 69 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 70 | evaluator.evaluateModule(source) as Promise; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pkl-community/pkl-typescript", 3 | "description": "Typescript bindings for Pkl", 4 | "version": "0.0.19", 5 | "main": "dist/src/index.js", 6 | "types": "dist/src/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/pkl-community/pkl-typescript.git" 10 | }, 11 | "publishConfig": { 12 | "@pkl-community:registry": "https://registry.npmjs.org" 13 | }, 14 | "author": "Pkl community Developers", 15 | "license": "Apache-2.0", 16 | "homepage": "https://github.com/pkl-community/pkl-typescript#readme", 17 | "scripts": { 18 | "typecheck": "tsc --noEmit", 19 | "lint": "eslint .", 20 | "lint:fix": "eslint . --fix", 21 | "build": "tsc -p tsconfig.dist.json && mkdir -p dist/codegen && cp -r codegen/src dist/codegen/src", 22 | "test:e2e": "jest", 23 | "test:codegen": "pkl test $(find ./codegen/src/ -name '*.test.pkl')", 24 | "test:snippet": "pkl test ./codegen/snippet-tests/test.pkl", 25 | "test": "npm run test:codegen && npm run test:snippet && npm run test:e2e", 26 | "dev": "tsx pkl-gen-typescript/main.ts", 27 | "gen-snippets": "npm run dev -- --settings-file codegen/snippet-tests/generator-settings.pkl -o codegen/snippet-tests/output codegen/snippet-tests/input/*.pkl" 28 | }, 29 | "devDependencies": { 30 | "@jest/globals": "^29.7.0", 31 | "@types/node": "^18.19.18", 32 | "@typescript-eslint/eslint-plugin": "^7.4.0", 33 | "@typescript-eslint/parser": "^7.4.0", 34 | "eslint": "^8.57.0", 35 | "eslint-config-prettier": "^9.1.0", 36 | "eslint-plugin-jest": "^27.9.0", 37 | "eslint-plugin-simple-import-sort": "^12.0.0", 38 | "jest": "^29.7.0", 39 | "prettier": "^3.2.5", 40 | "ts-jest": "^29.1.2", 41 | "tsx": "^4.7.1", 42 | "typescript": "^4.9.4" 43 | }, 44 | "dependencies": { 45 | "chalk": "^4.1.2", 46 | "cmd-ts": "^0.13.0", 47 | "consola": "^3.2.3", 48 | "msgpackr": "^1.10.1" 49 | }, 50 | "peerDependencies": { 51 | "@pkl-community/pkl": "*" 52 | }, 53 | "bin": { 54 | "pkl-gen-typescript": "./dist/bin/pkl-gen-typescript.js" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /codegen/src/internal/TypescriptMapping.pkl: -------------------------------------------------------------------------------- 1 | abstract module pkl.typescript.internal.TypescriptMapping 2 | 3 | import "TypescriptMapping.pkl" 4 | import "Type.pkl" 5 | import "pkl:reflect" 6 | import "utils.pkl" 7 | 8 | /// The TypeScript module path, e.g. `@myorg/myproj/appconfig` 9 | typescriptModule: String? 10 | 11 | /// The short name of the TypeScript package, e.g. `appconfig` 12 | typescriptModuleShort: String? = if (typescriptModule != null) utils.normalizeName(typescriptModule.split("/").last) else null 13 | 14 | /// The exposed simple name of this type 15 | name: String 16 | 17 | /// All names exposed by this mapping 18 | names: List 19 | 20 | seenMappings: List 21 | 22 | /// The source for this mapping 23 | source: reflect.TypeDeclaration 24 | 25 | /// The exposed TypeScript type 26 | type: Type 27 | 28 | /// The set of names already seen prior to creating mappings for this module. 29 | existingNames: List = seenMappings.flatMap((it) -> 30 | if (it.typescriptModule == module.typescriptModule) it.names 31 | else List() 32 | ) 33 | 34 | class TypeAlias extends TypescriptMapping { 35 | local self = this 36 | 37 | alias: reflect.TypeAlias = self.source as reflect.TypeAlias 38 | 39 | name = utils.toTypescriptName(self.source) 40 | 41 | names = List(name) 42 | 43 | type = new Type.Declared { 44 | typeName = name 45 | `module` = self.typescriptModule 46 | } 47 | } 48 | 49 | class Class extends TypescriptMapping { 50 | local self = this 51 | 52 | clazz: reflect.Class = self.source as reflect.Class 53 | 54 | names = List(interface?.name).filterNonNull() as List 55 | 56 | type = if (interface != null) 57 | // Default: produce interfaces for Pkl classes 58 | interface.type 59 | else 60 | // What other kinds of output could a Pkl class produce? 61 | // Not a struct, that's for sure! 62 | struct.type 63 | 64 | name = utils.toTypescriptName(clazz) 65 | 66 | interface: Interface? = 67 | new Interface { 68 | name = self.name 69 | type = new Type.Declared { 70 | typeName = self.name 71 | importPath = self.typescriptModule 72 | `module` = self.typescriptModuleShort 73 | } 74 | } 75 | } 76 | 77 | class Interface { 78 | name: String 79 | type: Type 80 | } 81 | -------------------------------------------------------------------------------- /codegen/src/Generator.pkl: -------------------------------------------------------------------------------- 1 | /// Generates TypeScript sources from Pkl 2 | @typescript.Module { name = "pkl_gen_typescript" } 3 | @ModuleInfo { minPklVersion = "0.25.0" } 4 | module pkl.typescript.Generator 5 | 6 | import "pkl:reflect" 7 | import "typescript.pkl" 8 | import "internal/gatherer.pkl" 9 | import "internal/TypescriptMapping.pkl" 10 | import "internal/TypescriptModule.pkl" 11 | import "internal/utils.pkl" 12 | 13 | /// The module that should be generated. 14 | moduleToGenerate: Module 15 | 16 | function getTypescriptModuleName(decl: reflect.TypeDeclaration): String? = 17 | decl.enclosingDeclaration 18 | .annotations 19 | .findOrNull((it) -> it.getClass().toString() == "pkl.typescript.typescript#Module") 20 | ?.name 21 | 22 | function gatherClasses(decl: List): Mixin> = 23 | (acc) -> 24 | decl 25 | .filter((it) -> it is reflect.Class) 26 | .fold(acc, (accum, it) -> accum.add(new TypescriptMapping.Class { 27 | typescriptModule = getTypescriptModuleName(it) 28 | source = it 29 | seenMappings = accum 30 | })) 31 | 32 | function gatherTypeAliases(decl: List): Mixin> = 33 | (acc) -> 34 | decl 35 | .filter((it) -> it is reflect.TypeAlias) 36 | .fold(acc, (accum, it) -> accum.add(new TypescriptMapping.TypeAlias { 37 | typescriptModule = getTypescriptModuleName(it) 38 | source = it 39 | seenMappings = accum 40 | })) 41 | 42 | local allMappings: List = 43 | let (clazz = reflect.Module(moduleToGenerate).moduleClass) 44 | let (declarations = gatherer.gatherTypeDeclarations(clazz, List())) 45 | List() |> 46 | gatherClasses(declarations) |> 47 | gatherTypeAliases(declarations) 48 | 49 | local modules = allMappings 50 | .groupBy((it) -> it.typescriptModule) 51 | .mapValues((`_moduleName`: String?, _mappings: List) -> new TypescriptModule { 52 | typescriptModule = _moduleName 53 | `module` = _mappings.first.source.enclosingDeclaration 54 | mappings = _mappings 55 | }) 56 | 57 | output { 58 | files { 59 | for (_, m in modules) { 60 | ...m.output.files!! 61 | } 62 | } 63 | text = throw("Generator.pkl only produces multiple-file output. Try running again with the -m flag.") 64 | } 65 | -------------------------------------------------------------------------------- /src/types/pkl.ts: -------------------------------------------------------------------------------- 1 | export type Any = null | AnyObject | Map | string | number | bigint | boolean 2 | export type AnyObject = 3 | BaseObject 4 | | Dynamic 5 | | Map 6 | | Any[] 7 | | Set 8 | | Duration 9 | | DataSize 10 | | Pair 11 | | IntSeq 12 | | Regex 13 | | {} 14 | 15 | // BaseObject is the TS representation of `pkl.base#Object`. 16 | export type BaseObject = { 17 | // object properties 18 | [k: string]: Any 19 | } 20 | 21 | // Dynamic is the TS representation of `pkl.base#Dynamic`. 22 | export type Dynamic = { 23 | properties: { [key: string]: Any } 24 | entries: Map 25 | elements: Any[] 26 | } 27 | 28 | export type DataSizeUnit = "b" | "kb" | "kib" | "mb" | "mib" | "gb" | "gib" | "tb" | "tib" | "pb" | "pib" 29 | 30 | // DataSize is the TS representation of `pkl.base#DataSize`. 31 | // 32 | // It represents a quantity of binary data, represented by value (e.g. 30.5) and unit 33 | // (e.g. mb). 34 | export type DataSize = { 35 | // value is the value of this data size. 36 | value: number 37 | 38 | // unit is the unit of this data size. 39 | unit: DataSizeUnit 40 | } 41 | 42 | export type DurationUnit = "ns" | "us" | "ms" | "s" | "min" | "hour" | "d" 43 | 44 | // Duration is the TS representation of `pkl.base#Duration`. 45 | // 46 | // It represents an amount of time, represented by value (e.g. 30.5) and unit 47 | // (e.g. s). 48 | export type Duration = { 49 | // value is the value of this duration. 50 | value: number 51 | 52 | // unit is the unit of this duration. 53 | unit: DurationUnit 54 | } 55 | 56 | // IntSeq is the TS representation of `pkl.base#IntSeq`. 57 | // 58 | // This value exists for compatibility. IntSeq should preferrably be used as a way to describe 59 | // logic within a Pkl program, and not passed as data between Pkl and TS. 60 | export type IntSeq = { 61 | // start is the start of this seqeunce. 62 | start: number 63 | 64 | // end is the end of this seqeunce. 65 | end: number 66 | 67 | // step is the common difference of successive members of this sequence. 68 | step: number 69 | } 70 | 71 | // Regex is the TS representation of `pkl.base#Regex`. 72 | export type Regex = { 73 | // pattern is the regex pattern expression in string form. 74 | pattern: string 75 | } 76 | 77 | // Pair is the TS representation of `pkl.base#Pair`. 78 | export type Pair = [A, B] 79 | -------------------------------------------------------------------------------- /codegen/snippet-tests/output/04_with_class.pkl.ts: -------------------------------------------------------------------------------- 1 | /* This file was generated by `pkl-typescript` from Pkl module `04-withClass`. */ 2 | /* DO NOT EDIT! */ 3 | /* istanbul ignore file */ 4 | /* eslint-disable */ 5 | import * as pklTypescript from "@pkl-community/pkl-typescript" 6 | 7 | // Ref: Module root. 8 | export interface N04WithClass { 9 | value: MyCustomClass 10 | } 11 | 12 | // Ref: Pkl class `04-withClass.MyCustomClass`. 13 | export interface MyCustomClass { 14 | x: string 15 | 16 | y: number 17 | } 18 | 19 | // Ref: Pkl class `04-withClass.MyAbstractClass`. 20 | export interface MyAbstractClass { 21 | someString: string 22 | 23 | overrideableStringType: string 24 | 25 | overridableListing1: Array 26 | 27 | overridableListing2: Array 28 | 29 | overridableMap1: Map 30 | 31 | overridableMap2: Map 32 | 33 | overridableUnion1: number | string | Array 34 | 35 | overridableUnion2: number | string 36 | } 37 | 38 | // Ref: Pkl class `04-withClass.MyConcreteClass`. 39 | export interface MyConcreteClass extends MyAbstractClass { 40 | anotherString: string 41 | 42 | overrideableStringType: "string literal type" 43 | 44 | overridableListing1: Array 45 | 46 | overridableListing2: Array 47 | 48 | overridableMap1: Map 49 | 50 | overridableMap2: Map 51 | 52 | overridableUnion1: string | number 53 | 54 | overridableUnion2: string 55 | } 56 | 57 | // Ref: Pkl class `04-withClass.MyOpenClass`. 58 | export interface MyOpenClass { 59 | someString: string 60 | } 61 | 62 | // Ref: Pkl class `04-withClass.MySubclassOfOpen`. 63 | export interface MySubclassOfOpen extends MyOpenClass { 64 | someInt: number 65 | } 66 | 67 | // LoadFromPath loads the pkl module at the given path and evaluates it into a N04WithClass 68 | export const loadFromPath = async (path: string): Promise => { 69 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 70 | try { 71 | const result = await load(evaluator, pklTypescript.FileSource(path)); 72 | return result 73 | } finally { 74 | evaluator.close() 75 | } 76 | }; 77 | 78 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise => 79 | evaluator.evaluateModule(source) as Promise; 80 | -------------------------------------------------------------------------------- /src/types/incoming.ts: -------------------------------------------------------------------------------- 1 | import { 2 | codeEvaluateLog, 3 | codeEvaluateRead, 4 | codeEvaluateReadModule, 5 | codeEvaluateResponse, 6 | codeListModulesRequest, 7 | codeListResourcesRequest, 8 | codeNewEvaluatorResponse 9 | } from "./codes"; 10 | 11 | export type IncomingMessage = 12 | CreateEvaluatorResponse 13 | | EvaluateResponse 14 | | ReadResource 15 | | ReadModule 16 | | Log 17 | | ListResources 18 | | ListModules 19 | 20 | export type CreateEvaluatorResponse = { 21 | requestId: bigint 22 | evaluatorId: bigint 23 | error: string 24 | code: typeof codeNewEvaluatorResponse, 25 | } 26 | 27 | export type EvaluateResponse = { 28 | requestId: bigint 29 | evaluatorId: bigint 30 | result: Uint8Array 31 | error?: string 32 | code: typeof codeEvaluateResponse, 33 | } 34 | 35 | export type ReadResource = { 36 | requestId: bigint 37 | evaluatorId: bigint 38 | uri: string 39 | code: typeof codeEvaluateRead, 40 | } 41 | 42 | export type ReadModule = { 43 | requestId: bigint 44 | evaluatorId: bigint 45 | uri: string 46 | code: typeof codeEvaluateReadModule, 47 | } 48 | 49 | export type Log = { 50 | evaluatorId: bigint 51 | level: number 52 | message: string 53 | frameUri: string 54 | code: typeof codeEvaluateLog, 55 | } 56 | 57 | export type ListResources = { 58 | requestId: bigint 59 | evaluatorId: bigint 60 | uri: string 61 | code: typeof codeListResourcesRequest, 62 | } 63 | 64 | export type ListModules = { 65 | requestId: bigint 66 | evaluatorId: bigint 67 | uri: string 68 | code: typeof codeListModulesRequest, 69 | } 70 | 71 | export function decode(incoming: unknown): IncomingMessage { 72 | const [code, map] = incoming as [number, Record] 73 | const value = map 74 | switch (code) { 75 | case codeEvaluateResponse: 76 | return {...value, code: codeEvaluateResponse} as EvaluateResponse; 77 | case codeEvaluateLog: 78 | return {...value, code: codeEvaluateLog} as Log; 79 | case codeNewEvaluatorResponse: 80 | return {...value, code: codeNewEvaluatorResponse} as CreateEvaluatorResponse; 81 | case codeEvaluateRead: 82 | return {...value, code: codeEvaluateRead} as ReadResource; 83 | case codeEvaluateReadModule: 84 | return {...value, code: codeEvaluateReadModule} as ReadModule; 85 | case codeListResourcesRequest: 86 | return {...value, code: codeListResourcesRequest} as ListResources; 87 | case codeListModulesRequest: 88 | return {...value, code: codeListModulesRequest} as ListModules; 89 | default: 90 | throw new Error(`Unknown code: ${code}`) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /codegen/src/tests/ClassGen.test.pkl: -------------------------------------------------------------------------------- 1 | amends "pkl:test" 2 | 3 | import ".../internal/ClassGen.pkl" 4 | import ".../internal/TypescriptMapping.pkl" 5 | import "pkl:reflect" 6 | 7 | local class Person { 8 | name: String 9 | hobbies: List 10 | hidden talent: String 11 | } 12 | 13 | facts { 14 | ["getFields ignores hidden members"] { 15 | local fields = ClassGen.getFields(reflect.Class(Person), List()) 16 | fields.keys == Set("name", "hobbies") 17 | } 18 | } 19 | 20 | local class EmptyClass {} 21 | 22 | local class OnePropertyClass { 23 | name: String 24 | } 25 | 26 | local class MultiPropertyClass { 27 | name: String 28 | age: Int32 29 | } 30 | 31 | local class WithListingClass { 32 | name: String 33 | hobbies: Listing 34 | } 35 | 36 | local class WithMappingClass { 37 | name: String 38 | siblingAges: Mapping 39 | } 40 | 41 | local class WithNestedMappingClass { 42 | name: String 43 | siblings: Mapping> 44 | } 45 | 46 | local class NestedClass { 47 | name: String 48 | friend: MultiPropertyClass 49 | } 50 | 51 | local class MappingToNestedClass { 52 | name: String 53 | familyMembers: Mapping 54 | } 55 | 56 | local class RecursiveClass { 57 | name: String 58 | bestFriend: RecursiveClass 59 | } 60 | 61 | local class RecursiveListingClass { 62 | name: String 63 | friends: Listing 64 | } 65 | 66 | local class RecursiveMappingClass { 67 | name: String 68 | friends: Mapping 69 | } 70 | 71 | examples { 72 | for (name, clazz in new Mapping { 73 | ["Empty class"] = EmptyClass 74 | ["One-property class"] = OnePropertyClass 75 | ["Multi-property class"] = MultiPropertyClass 76 | ["Class with a listing proprety"] = WithListingClass 77 | ["Class with a mapping property"] = WithMappingClass 78 | ["Class with a nested mapping property"] = WithNestedMappingClass 79 | 80 | // TODO(Jason): Below examples are hitting `Cannot generate type XxxxClass as TypeScript` error from typegen.pkl 81 | // 82 | // ["Class with a nested class property"] = NestedClass 83 | // ["Class with a mapping property to another class"] = MappingToNestedClass 84 | // ["Basic recursive class"] = RecursiveClass 85 | // ["Class with a recursive listing of itself"] = RecursiveListingClass 86 | // ["Class with a recursive mapping of itself"] = RecursiveMappingClass 87 | }) { 88 | [name] { 89 | ((ClassGen) { 90 | mapping = (TypescriptMapping.Class) { 91 | source = reflect.Class(clazz) 92 | typescriptModule = "@myorg/myproj/appconfig" 93 | } 94 | }).contents 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/types/outgoing.ts: -------------------------------------------------------------------------------- 1 | import * as msgpackr from "msgpackr"; 2 | import { 3 | codeCloseEvaluator, 4 | codeEvaluate, 5 | codeEvaluateReadModuleResponse, 6 | codeEvaluateReadResponse, 7 | codeListModulesResponse, 8 | codeListResourcesResponse, 9 | codeNewEvaluator 10 | } from "./codes"; 11 | 12 | export type OutgoingMessage = 13 | CreateEvaluator 14 | | CloseEvaluator 15 | | Evaluate 16 | | ReadResourceResponse 17 | | ReadModuleResponse 18 | | ListResourcesResponse 19 | | ListModulesResponse 20 | 21 | export type ResourceReader = { 22 | scheme: string, 23 | hasHierarchicalUris: boolean, 24 | isGlobbable: boolean, 25 | } 26 | 27 | export type ModuleReader = { 28 | scheme: string, 29 | hasHierarchicalUris: boolean, 30 | isGlobbable: boolean, 31 | isLocal: boolean, 32 | } 33 | 34 | export type CreateEvaluator = { 35 | requestId: bigint, 36 | clientResourceReaders?: ResourceReader[], 37 | clientModuleReaders?: ModuleReader[], 38 | modulePaths?: string[], 39 | env?: Record, 40 | properties?: Record, 41 | outputFormat?: string, 42 | allowedModules?: string[], 43 | allowedResources?: string[], 44 | rootDir?: string, 45 | cacheDir?: string, 46 | project?: ProjectOrDependency, 47 | code: typeof codeNewEvaluator, 48 | } 49 | 50 | export type ProjectOrDependency = { 51 | packageUri?: string, 52 | type?: string, 53 | projectFileUri?: string 54 | checksums?: Checksums 55 | dependencies?: Record 56 | } 57 | 58 | export type Checksums = { 59 | checksums: string 60 | } 61 | 62 | export type CloseEvaluator = { 63 | evaluatorId: bigint, 64 | code: typeof codeCloseEvaluator, 65 | } 66 | 67 | export type Evaluate = { 68 | requestId: bigint 69 | evaluatorId: bigint 70 | moduleUri: string 71 | moduleText?: string 72 | expr?: string 73 | code: typeof codeEvaluate, 74 | } 75 | 76 | export type ReadResourceResponse = { 77 | requestId: bigint, 78 | evaluatorId: bigint, 79 | code: typeof codeEvaluateReadResponse, 80 | } & ({ contents: Uint8Array } | { error: string }) 81 | 82 | export type ReadModuleResponse = { 83 | requestId: bigint, 84 | evaluatorId: bigint, 85 | code: typeof codeEvaluateReadModuleResponse, 86 | } & ({ contents: string } | { error: string }) 87 | 88 | export type ListResourcesResponse = { 89 | requestId: bigint, 90 | evaluatorId: bigint, 91 | code: typeof codeListResourcesResponse, 92 | } & ({ pathElements: PathElement[] } | { error: string }) 93 | 94 | export type ListModulesResponse = { 95 | requestId: bigint, 96 | evaluatorId: bigint, 97 | code: typeof codeListModulesResponse, 98 | } & ({ pathElements: PathElement[] } | { error: string }) 99 | 100 | export type PathElement = { 101 | name: string, 102 | isDirectory: boolean, 103 | } 104 | 105 | export function packMessage(encoder: msgpackr.Encoder, msg: OutgoingMessage): Buffer { 106 | const {code, ...rest} = msg; 107 | const enc = encoder.encode([code, rest]) 108 | return enc 109 | } 110 | -------------------------------------------------------------------------------- /pkl-gen-typescript/generate.ts: -------------------------------------------------------------------------------- 1 | import { mkdir, mkdtemp, writeFile } from "fs/promises"; 2 | import { tmpdir } from "os"; 3 | import { isAbsolute, join, sep } from "path"; 4 | import { cwd } from "process"; 5 | import { Evaluator } from "src/evaluator/evaluator"; 6 | import { pathToFileURL } from "url"; 7 | import type { GeneratorSettings } from "./generated"; 8 | import chalk from "chalk"; 9 | import { consola } from "consola"; 10 | 11 | const toAbsolutePath = (path: string) => 12 | isAbsolute(path) ? path : join(cwd(), path); 13 | 14 | export async function generateTypescript( 15 | evaluator: Evaluator, 16 | pklModulePaths: string[], 17 | settings: GeneratorSettings 18 | ) { 19 | consola.start( 20 | `Generating TypeScript sources for modules ${chalk.cyan( 21 | pklModulePaths.join(", ") 22 | )}` 23 | ); 24 | 25 | pklModulePaths = pklModulePaths.map(toAbsolutePath); 26 | 27 | if (settings.generatorScriptPath) { 28 | settings.generatorScriptPath = settings.generatorScriptPath.includes(":") 29 | ? settings.generatorScriptPath 30 | : join(cwd(), settings.generatorScriptPath); 31 | 32 | consola.warn( 33 | `Using custom generator script: ${chalk.cyan( 34 | settings.generatorScriptPath 35 | )}` 36 | ); 37 | } else { 38 | // TODO(Jason): If this is bundled, do we need 39 | // to find the path better than this? 40 | settings.generatorScriptPath = join( 41 | __dirname, 42 | "../codegen/src/Generator.pkl" 43 | ); 44 | } 45 | 46 | const tmpDir = await mkdtemp(`${tmpdir}${sep}`); 47 | 48 | const outputDir = settings.outputDirectory 49 | ? toAbsolutePath(settings.outputDirectory) 50 | : join(cwd(), ".out"); 51 | await mkdir(outputDir, { recursive: true }); 52 | 53 | for (let [index, pklInputModule] of pklModulePaths.entries()) { 54 | // TODO: This was taken from Swift - is it missing anything from the Go template? 55 | // https://github.com/apple/pkl-go/blob/main/cmd/pkl-gen-go/pkg/template.gopkl 56 | const moduleToEvaluate = ` 57 | amends "${settings.generatorScriptPath}" 58 | 59 | import "${pklInputModule}" as theModule 60 | 61 | moduleToGenerate = theModule 62 | `; 63 | 64 | consola.level >= 4 /* consola.debug is level 4 */ 65 | ? consola.box(` 66 | Evaluating temp Pkl module: 67 | --- 68 | ${moduleToEvaluate}`) 69 | : null; 70 | 71 | const tmpFilePath = join(tmpDir, `pkl-gen-typescript-${index}.pkl`); 72 | 73 | await writeFile(tmpFilePath, moduleToEvaluate, "utf-8"); 74 | const files = (await evaluator.evaluateOutputFiles({ 75 | uri: pathToFileURL(tmpFilePath), 76 | })) as unknown as Map; 77 | 78 | for (let [filename, contents] of files) { 79 | const path = join(outputDir, filename); 80 | 81 | if (!settings.dryRun) { 82 | await writeFile(path, contents, "utf-8"); 83 | } 84 | consola.success(path); 85 | } 86 | } 87 | 88 | // TODO: Validate/fix formatting with Prettier, like Go does with gofmt? 89 | // That, or put prettier-ignore/eslint-ignore directives at the top of the file. 90 | } 91 | -------------------------------------------------------------------------------- /src/evaluator/evaluator_exec.ts: -------------------------------------------------------------------------------- 1 | import {EvaluatorOptions, PreconfiguredOptions, withProject} from "./evaluator_options"; 2 | 3 | import {Evaluator} from "./evaluator"; 4 | import {loadProjectFromEvaluator} from "./project"; 5 | import {newEvaluatorManagerWithCommand} from "./evaluator_manager"; 6 | 7 | // newEvaluator returns an evaluator backed by a single EvaluatorManager. 8 | // Its manager gets closed when the evaluator is closed. 9 | // 10 | // If creating multiple evaluators, prefer using EvaluatorManager.NewEvaluator instead, 11 | // because it lessens the overhead of each successive evaluator. 12 | export function newEvaluator(opts: EvaluatorOptions): Promise { 13 | return newEvaluatorWithCommand([], opts) 14 | } 15 | 16 | // NewProjectEvaluator is an easy way to create an evaluator that is configured by the specified 17 | // projectDir. 18 | // 19 | // It is similar to running the `pkl eval` or `pkl test` CLI command with a set `--project-dir`. 20 | // 21 | // When using project dependencies, they must first be resolved using the `pkl project resolve` 22 | // CLI command. 23 | export function newProjectEvaluator(projectDir: string, opts: EvaluatorOptions): Promise { 24 | return newProjectEvaluatorWithCommand(projectDir, [], opts) 25 | } 26 | 27 | // NewProjectEvaluatorWithCommand is like NewProjectEvaluator, but also accepts the Pkl command to run. 28 | // 29 | // The first element in pklCmd is treated as the command to run. 30 | // Any additional elements are treated as arguments to be passed to the process. 31 | // pklCmd is treated as the base command that spawns Pkl. 32 | // For example, the below snippet spawns the command /opt/bin/pkl. 33 | // 34 | // NewProjectEvaluatorWithCommand(context.Background(), []string{"/opt/bin/pkl"}, "/path/to/my/project") 35 | // 36 | // If creating multiple evaluators, prefer using EvaluatorManager.NewProjectEvaluator instead, 37 | // because it lessens the overhead of each successive evaluator. 38 | export async function newProjectEvaluatorWithCommand(projectDir: string, pklCmd: string[], opts: EvaluatorOptions): Promise { 39 | const manager = newEvaluatorManagerWithCommand(pklCmd) 40 | const projectEvaluator = await newEvaluator(PreconfiguredOptions) 41 | const project = await loadProjectFromEvaluator(projectEvaluator, projectDir+"/PklProject") 42 | return manager.newEvaluator({...withProject(project), ...opts}) 43 | } 44 | 45 | // newEvaluatorWithCommand is like NewEvaluator, but also accepts the Pkl command to run. 46 | // 47 | // The first element in pklCmd is treated as the command to run. 48 | // Any additional elements are treated as arguments to be passed to the process. 49 | // pklCmd is treated as the base command that spawns Pkl. 50 | // For example, the below snippet spawns the command /opt/bin/pkl. 51 | // 52 | // NewEvaluatorWithCommand(context.Background(), []string{"/opt/bin/pkl"}) 53 | // 54 | // If creating multiple evaluators, prefer using EvaluatorManager.NewEvaluator instead, 55 | // because it lessens the overhead of each successive evaluator. 56 | export function newEvaluatorWithCommand(pklCmd: string[], opts: EvaluatorOptions): Promise { 57 | const manager = newEvaluatorManagerWithCommand(pklCmd) 58 | return manager.newEvaluator(opts) 59 | } 60 | -------------------------------------------------------------------------------- /codegen/src/internal/Type.pkl: -------------------------------------------------------------------------------- 1 | /// Representation of a type in TypeScript. 2 | @Unlisted 3 | abstract module pkl.typescript.internal.Type 4 | 5 | import "Type.pkl" 6 | import "utils.pkl" 7 | 8 | /// The imports required by this type. 9 | imports: List 10 | 11 | /// The TypeScript representation of this type. 12 | /// 13 | /// [typescriptModule] is the full path of the module that this type appears in. 14 | abstract function render(typescriptModule: String): String 15 | 16 | class Record extends Type { 17 | key: Type 18 | 19 | elem: Type 20 | 21 | imports = elem.imports 22 | 23 | function render(typescriptModule: String) = 24 | "Record<\(key.render(typescriptModule)), \(elem.render(typescriptModule))>" 25 | } 26 | 27 | class Map extends Type { 28 | key: Type 29 | 30 | elem: Type 31 | 32 | imports = elem.imports 33 | 34 | function render(typescriptModule: String?) = 35 | "Map<\(key.render(typescriptModule)), \(elem.render(typescriptModule))>" 36 | } 37 | 38 | class Array extends Type { 39 | elem: Type 40 | 41 | imports = elem.imports 42 | 43 | function render(typescriptModule: String?) = "Array<\(elem.render(typescriptModule))>" 44 | } 45 | 46 | class Set extends Type { 47 | elem: Type 48 | 49 | imports = elem.imports 50 | 51 | function render(typescriptModule: String?) = "Set<\(elem.render(typescriptModule))>" 52 | } 53 | 54 | class Pair extends Type { 55 | elems: List 56 | 57 | imports = elems.toList().flatMap((e) -> e.imports) 58 | 59 | function render(typescriptModule: String?) = "pklTypescript.Pair<" + 60 | elems.map((e) -> e.render(typescriptModule)).join(", ") 61 | + ">" 62 | } 63 | 64 | class Tuple extends Type { 65 | elems: List 66 | 67 | imports = elems.toList().flatMap((e) -> e.imports) 68 | 69 | function render(typescriptModule: String?) = "readonly [" + 70 | elems.map((e) -> e.render(typescriptModule)).join(", ") 71 | + "]" 72 | } 73 | 74 | class Union extends Type { 75 | elems: List 76 | 77 | imports = elems.toList().flatMap((e) -> e.imports) 78 | 79 | function render(typescriptModule: String?) = 80 | elems.map((e) -> e.render(typescriptModule)).join(" | ") 81 | } 82 | 83 | class Nullable extends Type { 84 | elem: Type 85 | 86 | imports = elem.imports 87 | 88 | function render(typescriptModule: String?) = "\(elem.render(typescriptModule))|null" 89 | } 90 | 91 | class Declared extends Type { 92 | /// The full import path for this type. 93 | importPath: String? 94 | 95 | imports = (if (importPath != null) List(importPath) else List()) 96 | + if (typeArguments != null) typeArguments.flatMap((t) -> t.imports) else List() 97 | 98 | /// The module the type is found in 99 | `module`: String? 100 | 101 | /// The name of the type 102 | typeName: String 103 | 104 | /// The type arguments, if any. 105 | typeArguments: List? 106 | 107 | function renderBase(typescriptModule: String?) = 108 | if (`module` != null && typescriptModule != null && typescriptModule != importPath) "\(utils.normalizeName(`module`)).\(typeName)" 109 | else typeName 110 | 111 | function renderTypeArguments(typescriptModule: String?) = 112 | if (typeArguments == null) "" 113 | else "<" + typeArguments.map((t) -> t.render(typescriptModule)).join(", ") + ">" 114 | 115 | function render(typescriptModule: String?) = 116 | renderBase(typescriptModule) + renderTypeArguments(typescriptModule) 117 | } 118 | -------------------------------------------------------------------------------- /src/evaluator/reader.ts: -------------------------------------------------------------------------------- 1 | // Reader is the base implementation shared by a ResourceReader and a ModuleReader. 2 | interface Reader { 3 | // scheme returns the scheme part of the URL that this reader can read. 4 | scheme: string 5 | 6 | // isGlobbable tells if this reader supports globbing via Pkl's `import*` and `glob*` keywords 7 | isGlobbable: boolean 8 | 9 | // hasHierarchicalUris tells if the URIs handled by this reader are hierarchical. 10 | // Hierarchical URIs are URIs that have hierarchy elements like host, origin, query, and 11 | // fragment. 12 | // 13 | // A hierarchical URI must start with a "/" in its scheme specific part. For example, consider 14 | // the following two URIS: 15 | // 16 | // flintstone:/persons/fred.pkl 17 | // flintstone:persons/fred.pkl 18 | // 19 | // The first URI conveys name "fred.pkl" within parent "/persons/". The second URI 20 | // conveys the name "persons/fred.pkl" with no hierarchical meaning. 21 | hasHierarchicalUris: boolean 22 | 23 | // listElements returns the list of elements at a specified path. 24 | // If HasHierarchicalUris is false, path will be empty and ListElements should return all 25 | // available values. 26 | // 27 | // This method is only called if it is hierarchical and local, or if it is globbable. 28 | listElements(url: URL): PathElement[] 29 | } 30 | 31 | // PathElement is an element within a base URI. 32 | // 33 | // For example, a PathElement with name "bar.txt" and is not a directory at base URI "file:///foo/" 34 | // implies URI resource `file:///foo/bar.txt`. 35 | type PathElement = { 36 | // name is the name of the path element. 37 | name: string 38 | 39 | // isDirectory tells if the path element is a directory. 40 | isDirectory: boolean 41 | } 42 | 43 | // ResourceReader is a custom resource reader for Pkl. 44 | // 45 | // A ResourceReader registers the scheme that it is responsible for reading via Reader.Scheme. For 46 | // example, a resource reader can declare that it reads a resource at secrets:MY_SECRET by returning 47 | // "secrets" when Reader.Scheme is called. 48 | // 49 | // Resources are cached by Pkl for the lifetime of an Evaluator. Therefore, cacheing is not needed 50 | // on the TS side as long as the same Evaluator is used. 51 | // 52 | // Resources are read via the following Pkl expressions: 53 | // 54 | // read("myscheme:myresourcee") 55 | // read?("myscheme:myresource") 56 | // read*("myscheme:pattern*") // only if the resource is globabble 57 | // 58 | // To provide a custom reader, register it on EvaluatorOptions.ResourceReaders when building 59 | // an Evaluator. 60 | export interface ResourceReader extends Reader { 61 | // read reads the byte contents of this resource. 62 | read(url: URL): Uint8Array 63 | } 64 | 65 | // ModuleReader is a custom module reader for Pkl. 66 | // 67 | // A ModuleReader registers the scheme that it is responsible for reading via Reader.Scheme. For 68 | // example, a module reader can declare that it reads a resource at myscheme:myFile.pkl by returning 69 | // "myscheme" when Reader.Scheme is called. 70 | // 71 | // Modules are cached by Pkl for the lifetime of an Evaluator. Therefore, cacheing is not needed 72 | // on the TS side as long as the same Evaluator is used. 73 | // 74 | // Modules are read in Pkl via the import declaration: 75 | // 76 | // import "myscheme:/myFile.pkl" 77 | // import* "myscheme:/*.pkl" // only when the reader is globbable 78 | // 79 | // Or via the import expression: 80 | // 81 | // import("myscheme:myFile.pkl") 82 | // import*("myscheme:/myFile.pkl") // only when the reader is globbable 83 | // 84 | // To provide a custom reader, register it on EvaluatorOptions.ModuleReaders when building 85 | // an Evaluator. 86 | export interface ModuleReader extends Reader { 87 | // isLocal tells if the resources represented by this reader is considered local to the runtime. 88 | // A local module reader enables resolving triple-dot imports. 89 | isLocal: boolean 90 | 91 | // read reads the string contents of this module. 92 | read(url: URL): string 93 | } 94 | -------------------------------------------------------------------------------- /codegen/src/internal/TypescriptModule.pkl: -------------------------------------------------------------------------------- 1 | import "pkl:reflect" 2 | import "TypescriptMapping.pkl" 3 | import "Gen.pkl" 4 | import "ClassGen.pkl" 5 | import "TypeAliasGen.pkl" 6 | import "utils.pkl" 7 | 8 | `module`: reflect.Module 9 | 10 | /// All mappings 11 | mappings: List 12 | 13 | typescriptModule: String? 14 | 15 | local moduleMappings: List = 16 | mappings.filter((it) -> it.typescriptModule == typescriptModule) 17 | 18 | local moduleClass = moduleMappings[0] 19 | 20 | local function describeLocation(src: reflect.TypeDeclaration) = 21 | let (memberType = 22 | if (src is reflect.Class && src.enclosingDeclaration.moduleClass == src) "module" 23 | else if (src is reflect.Class) "class" 24 | else "typealias" 25 | ) 26 | "* \(memberType) `\(src.reflectee)` (\(src.location.displayUri))" 27 | 28 | 29 | local function hasUniqueNames(): Boolean = 30 | let (names = moduleMappings.map((it) -> it.name)) 31 | if (names.isDistinct) true 32 | else 33 | let (duplicateNames = moduleMappings.filter((it) -> moduleMappings.count((m) -> m.name == it.name) > 1)) 34 | let (locations = duplicateNames.map((it) -> describeLocation(it.source)).join("\n")) 35 | throw(""" 36 | Conflict: multiple Pkl declarations compute to TypeScript name `\(duplicateNames.first.name)`. 37 | 38 | To resolve this conflict, add a `@typescript.Name` annotation to any of the following declarations: 39 | 40 | \(locations) 41 | 42 | For example: 43 | 44 | ``` 45 | @typescript.Name { value = "CrabCakes" } 46 | class Crab_Cakes 47 | ``` 48 | """) 49 | 50 | local generated: List(hasUniqueNames()) = 51 | moduleMappings.map((it) -> 52 | if (it is TypescriptMapping.TypeAlias) 53 | new TypeAliasGen { 54 | mappings = module.mappings 55 | mapping = it 56 | } 57 | else 58 | new ClassGen { 59 | mappings = module.mappings 60 | mapping = it 61 | } 62 | ) 63 | 64 | local imports = generated 65 | .filter((it) -> it is ClassGen) 66 | .map((it) -> it.imports) 67 | .flatten() 68 | .distinct 69 | 70 | contents: String = new Listing { 71 | """ 72 | /* This file was generated by `pkl-typescript` from Pkl module `\(`module`.moduleClass.enclosingDeclaration.name)`. */ 73 | /* DO NOT EDIT! */ 74 | /* istanbul ignore file */ 75 | /* eslint-disable */ 76 | """ 77 | 78 | utils.renderImports(imports) 79 | "" 80 | 81 | for (gen in generated) { 82 | gen.contents 83 | "" 84 | } 85 | 86 | when (!`module`.modifiers.contains("open") && !`module`.modifiers.contains("abstract")) { 87 | """ 88 | // LoadFromPath loads the pkl module at the given path and evaluates it into a \(moduleClass.name) 89 | export const loadFromPath = async (path: string): Promise<\(moduleClass.type.render(moduleClass.typescriptModule))> => { 90 | const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions); 91 | try { 92 | const result = await load(evaluator, pklTypescript.FileSource(path)); 93 | return result 94 | } finally { 95 | evaluator.close() 96 | } 97 | }; 98 | 99 | export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise<\(moduleClass.type.render(moduleClass.typescriptModule))> => 100 | evaluator.evaluateModule(source) as Promise<\(moduleClass.type.render(moduleClass.typescriptModule))>; 101 | 102 | """ 103 | } 104 | }.join("\n") 105 | 106 | 107 | output { 108 | files { 109 | // Snake-case the generated filenames, as per the Google TypeScript styleguide: 110 | // https://google.github.io/styleguide/tsguide.html#identifiers-imports 111 | // > "Module namespace imports are lowerCamelCase while files are snake_case" 112 | ["\(utils.snakeCase(`module`.name)).pkl.ts"] { 113 | text = contents 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /codegen/src/tests/typegen.test.pkl: -------------------------------------------------------------------------------- 1 | amends "pkl:test" 2 | 3 | import "pkl:reflect" 4 | 5 | import "../internal/typegen.pkl" 6 | 7 | local class Maps { 8 | res1: Map 9 | res2: Map> 10 | res3: Mapping 11 | res4: Mapping> 12 | res5: Mapping 13 | res6: Mapping 14 | } 15 | 16 | local reflectedMaps = reflect.Class(Maps) 17 | 18 | local class Arrays { 19 | res1: List 20 | res2: List> 21 | res3: Listing 22 | res4: Listing> 23 | res5: List 24 | res6: Listing 25 | } 26 | 27 | local reflectedArrays = reflect.Class(Arrays) 28 | 29 | local class Nullables { 30 | res1: String? 31 | res2: Boolean? 32 | res3: Listing 33 | res4: Listing? 34 | res5: Listing? 35 | res6: Mapping 36 | res7: Mapping? 37 | res8: Mapping? 38 | } 39 | 40 | local reflectedNullables = reflect.Class(Nullables) 41 | 42 | local class Pairs { 43 | res1: Pair 44 | res2: Pair 45 | } 46 | 47 | local reflectedPairs = reflect.Class(Pairs) 48 | 49 | local class Nothing { 50 | res1: nothing 51 | } 52 | 53 | local nothingType: reflect.Type = reflect.Class(Nothing).properties["res1"].type 54 | 55 | local mod = reflect.Module(module).moduleClass 56 | 57 | local function generateType(typ: reflect.Type) = typegen.generateType(typ, mod, List()).render("") 58 | 59 | facts { 60 | ["basic types"] { 61 | generateType(reflect.stringType) == "string" 62 | generateType(reflect.booleanType) == "boolean" 63 | generateType(reflect.int8Type) == "number" 64 | generateType(reflect.int16Type) == "number" 65 | generateType(reflect.int32Type) == "number" 66 | generateType(reflect.intType) == "number" 67 | generateType(reflect.floatType) == "number" 68 | generateType(reflect.uint8Type) == "number" 69 | generateType(reflect.uint16Type) == "number" 70 | generateType(reflect.uint32Type) == "number" 71 | generateType(reflect.uintType) == "number" 72 | generateType(reflect.anyType) == "pklTypescript.Any" 73 | generateType(reflect.dynamicType) == "pklTypescript.Dynamic" 74 | generateType(reflect.dataSizeType) == "pklTypescript.DataSize" 75 | generateType(reflect.durationType) == "pklTypescript.Duration" 76 | generateType(nothingType) == "never" 77 | generateType(reflect.DeclaredType(reflect.TypeAlias(Char))) == "string" 78 | generateType(reflect.DeclaredType(reflect.Class(Null))) == "null" 79 | } 80 | ["maps"] { 81 | generateType(reflectedMaps.properties["res1"].type) == "Map" 82 | generateType(reflectedMaps.properties["res2"].type) == "Map>" 83 | generateType(reflectedMaps.properties["res3"].type) == "Map" 84 | generateType(reflectedMaps.properties["res4"].type) == "Map>" 85 | } 86 | ["arrays"] { 87 | generateType(reflectedArrays.properties["res1"].type) == "Array" 88 | generateType(reflectedArrays.properties["res2"].type) == "Array>" 89 | generateType(reflectedArrays.properties["res3"].type) == "Array" 90 | generateType(reflectedArrays.properties["res4"].type) == "Array>" 91 | } 92 | ["nullables"] { 93 | generateType(reflectedNullables.properties["res1"].type) == "string|null" 94 | generateType(reflectedNullables.properties["res2"].type) == "boolean|null" 95 | generateType(reflectedNullables.properties["res3"].type) == "Array" 96 | generateType(reflectedNullables.properties["res4"].type) == "Array|null" 97 | generateType(reflectedNullables.properties["res5"].type) == "Array|null" 98 | generateType(reflectedNullables.properties["res6"].type) == "Map" 99 | generateType(reflectedNullables.properties["res7"].type) == "Map|null" 100 | generateType(reflectedNullables.properties["res8"].type) == "Map|null" 101 | } 102 | ["pairs"] { 103 | generateType(reflectedPairs.properties["res1"].type) == "pklTypescript.Pair" 104 | generateType(reflectedPairs.properties["res2"].type) == "pklTypescript.Pair" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /codegen/src/internal/gatherer.pkl: -------------------------------------------------------------------------------- 1 | // This is 100% copy-pasted from pkl-go and pkl-swift. 2 | // It is identical in those two packages. 3 | module pkl.typescript.internal.gatherer 4 | 5 | import "pkl:reflect" 6 | import "typegen.pkl" 7 | 8 | /// Given a [reflect.TypeDeclaration], collect all referenced classes or typealiases. 9 | /// 10 | /// Looks in properties, super classes, class/typealias members of enclosing modules, and generic 11 | /// type arguments. 12 | /// 13 | /// This omits core built-in types. 14 | function gatherTypeDeclarations( 15 | decl: reflect.TypeDeclaration, 16 | seen: List 17 | ): List = 18 | if (seen.contains(decl)) seen 19 | else if (typegen.mappedTypes.containsKey(decl.reflectee)) seen 20 | else if (decl is reflect.Class) 21 | if (isAnnotationClass(decl) || isBuiltinClass(decl)) seen 22 | else 23 | seen.add(decl) 24 | |> gatherPropertiesDeclarations(decl) 25 | |> gatherSuperDeclarations(decl) 26 | |> gatherModuleClasses(decl) 27 | |> gatherModuleTypeAliases(decl) 28 | |> gatherModule(decl) 29 | else if (decl is reflect.TypeAlias) 30 | seen.add(decl) |> gatherTypeArguments(decl) 31 | else seen 32 | 33 | /// Tells if this class is part of the stdlib. 34 | function isBuiltinClass(clazz: reflect.Class): Boolean = clazz.enclosingDeclaration.uri.startsWith("pkl:") 35 | 36 | function gatherTypeDeclarationsFromType( 37 | type: reflect.Type, 38 | enclosingDeclaration: reflect.TypeDeclaration, 39 | seen: List 40 | ): List = 41 | if (type is reflect.DeclaredType) 42 | let (referent = type.referent) 43 | if (referent is reflect.Class && isBuiltinClass(referent)) 44 | // TODO: why is this false? 45 | // let (_ = trace(reflect.DeclaredType(type.referent) == type)) 46 | type.typeArguments.fold(seen, (acc, it) -> gatherTypeDeclarationsFromType(it, referent, acc)) 47 | else 48 | gatherTypeDeclarations(referent, seen) 49 | else if (type is reflect.NullableType) 50 | gatherTypeDeclarationsFromType(type.member, enclosingDeclaration, seen) 51 | else if (type is reflect.UnionType) 52 | type.members.fold(seen, (acc, t) -> gatherTypeDeclarationsFromType(t, enclosingDeclaration, acc)) 53 | else 54 | // Remaining types: ModuleType, StringLiteralType, FunctionType, TypeVariable, NothingType. 55 | // None of these have a reference to a TypeDeclaration. 56 | seen 57 | 58 | function gatherPropertiesDeclarations(clazz: reflect.Class) = 59 | (seen: List) -> 60 | clazz.properties.values.fold( 61 | seen, 62 | (acc, p) -> gatherTypeDeclarationsFromType(p.type, clazz, acc) 63 | ) 64 | 65 | function gatherSuperDeclarations(clazz: reflect.Class) = 66 | (seen: List) -> 67 | let (superclass = clazz.superclass) 68 | if (superclass == null) 69 | seen 70 | else 71 | gatherTypeDeclarations(superclass, seen) 72 | 73 | function gatherModuleClasses(clazz: reflect.Class) = 74 | (seen: List) -> 75 | if (clazz.superclass != reflect.Class(Module)) 76 | seen 77 | else 78 | clazz.enclosingDeclaration.classes.fold( 79 | seen, 80 | (acc, _, clazz) -> gatherTypeDeclarations(clazz, acc) 81 | ) 82 | 83 | function gatherModuleTypeAliases(clazz: reflect.Class) = 84 | (seen: List) -> 85 | if (clazz.superclass != reflect.Class(Module)) 86 | seen 87 | else 88 | clazz.enclosingDeclaration.typeAliases.fold( 89 | seen, 90 | (acc, _, alias) -> gatherTypeDeclarations(alias, acc) 91 | ) 92 | 93 | function gatherModule(decl: reflect.TypeDeclaration) = 94 | (seen: List) -> 95 | gatherTypeDeclarations(decl.enclosingDeclaration.moduleClass, seen) 96 | 97 | function gatherTypeArguments(clazz: reflect.TypeDeclaration) = 98 | (seen: List) -> 99 | reflect.DeclaredType(clazz).typeArguments 100 | .fold(seen, (acc, it) -> gatherTypeDeclarationsFromType(it, clazz, acc)) 101 | 102 | function isAnnotationClass(clazz: reflect.Class): Boolean = 103 | if (clazz.reflectee == Annotation) true 104 | else if (clazz.superclass != null) isAnnotationClass(clazz.superclass!!) 105 | else false 106 | -------------------------------------------------------------------------------- /pkl-gen-typescript/main.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { boolean, command, flag, option, optional, restPositionals, run, string } from "cmd-ts"; 3 | import consola, { LogLevels } from "consola"; 4 | import { access } from "fs/promises"; 5 | import { join } from "path"; 6 | import { cwd } from "process"; 7 | import { pathToFileURL } from "url"; 8 | 9 | import { newEvaluator, PreconfiguredOptions } from "../src"; 10 | import { generateTypescript } from "./generate"; 11 | import type { GeneratorSettings } from "./generated"; 12 | import { load as loadGeneratorSettings } from "./generated"; 13 | 14 | export const cli = command({ 15 | name: "pkl-gen-typescript", 16 | args: { 17 | pklModules: restPositionals({ 18 | type: string, 19 | displayName: "Pkl module to evaluate", 20 | }), 21 | settingsFilePath: option({ 22 | type: optional(string), 23 | long: "settings-file", 24 | short: "s", 25 | description: "Path to the generator-settings.pkl file", 26 | }), 27 | dryRun: flag({ 28 | type: boolean, 29 | long: "dry-run", 30 | description: 31 | "Evaluate the Pkl modules and check that they could be generated, but do not write them anywhere", 32 | }), 33 | outputDirectory: option({ 34 | type: optional(string), 35 | long: "output-directory", 36 | short: "o", 37 | description: "Directory to write generated files into", 38 | }), 39 | verbose: flag({ 40 | type: boolean, 41 | long: "verbose", 42 | short: "v", 43 | description: "Enable debug logging", 44 | }), 45 | }, 46 | handler: async ({ pklModules, settingsFilePath, outputDirectory, dryRun, verbose }) => { 47 | /* 48 | Four ways to set the log level, in order of precedence: 49 | - CLI flag "-v", sets log level to "debug" 50 | - environment variable CONSOLA_LEVEL set to an integer 51 | - environment variable LOG_LEVEL set to a string that is a valid log level name 52 | - environment variable DEBUG, sets log level to "debug" 53 | - defaults to "info" 54 | */ 55 | const logLevel = verbose 56 | ? LogLevels.debug 57 | : (parseInt(process.env.CONSOLA_LEVEL ?? "") || null) ?? 58 | (process.env.LOG_LEVEL !== undefined && 59 | Object.keys(LogLevels).includes(process.env.LOG_LEVEL.toLowerCase())) 60 | ? LogLevels[process.env.LOG_LEVEL?.toLowerCase() as keyof typeof LogLevels] 61 | : process.env.DEBUG 62 | ? LogLevels.debug 63 | : LogLevels.info; 64 | consola.level = logLevel; 65 | 66 | if (!pklModules.length) { 67 | consola.error("You must provide at least one file to evaluate."); 68 | } 69 | 70 | const settingsFile: string | null = 71 | settingsFilePath ?? 72 | (await (async () => { 73 | // Check if there is a `generator-settings.pkl` in the current directory. 74 | const localSettingsFile = join(cwd(), "generator-settings.pkl"); 75 | try { 76 | await access(localSettingsFile); 77 | return localSettingsFile; 78 | } catch (err) { 79 | return null; 80 | } 81 | })()); 82 | 83 | if (settingsFile) { 84 | try { 85 | await access(settingsFile); 86 | consola.info(`Using settings file at ${chalk.cyan(settingsFile)}`); 87 | } catch (err) { 88 | consola.fatal(`Unable to read settings file at ${chalk.cyan(settingsFile)}.`); 89 | consola.debug(err); 90 | process.exit(1); 91 | } 92 | } 93 | 94 | const evaluator = await newEvaluator(PreconfiguredOptions); 95 | try { 96 | const settings = ( 97 | settingsFile 98 | ? await loadGeneratorSettings(evaluator, { 99 | uri: pathToFileURL(settingsFile), 100 | }) 101 | : // This ordering means that the generator-settings.pkl overrides CLI args 102 | // TODO: reverse this precedence, merge CLI args with settings file 103 | { 104 | dryRun, 105 | outputDirectory, 106 | } 107 | ) as GeneratorSettings; 108 | 109 | await generateTypescript(evaluator, pklModules, settings); 110 | } finally { 111 | evaluator.close(); 112 | } 113 | }, 114 | }); 115 | 116 | export default async function main(args: string[]) { 117 | return run(cli, args); 118 | } 119 | 120 | if (require.main === module) { 121 | void main(process.argv.slice(2)); 122 | } 123 | -------------------------------------------------------------------------------- /codegen/src/internal/typegen.pkl: -------------------------------------------------------------------------------- 1 | /// Utilities for generating TypeScript types from Pkl. 2 | module pkl.typescript.internal.typegen 3 | 4 | import "pkl:reflect" 5 | import "Type.pkl" 6 | import "TypescriptMapping.pkl" 7 | 8 | function generateType( 9 | type: reflect.Type, 10 | enclosing: reflect.TypeDeclaration, 11 | seenMappings: List 12 | ): Type = 13 | if (type is reflect.DeclaredType) 14 | generateDeclaredType(type, enclosing, seenMappings) 15 | else if (type is reflect.ModuleType) 16 | let (moduleClass = enclosing.enclosingDeclaration.moduleClass) 17 | generateType(reflect.DeclaredType(moduleClass), moduleClass, seenMappings) 18 | else if (type is reflect.UnionType) generateUnionType(type, enclosing, seenMappings) 19 | else if (type is reflect.NullableType) 20 | let (_elem = generateType(type.member, enclosing, seenMappings)) 21 | new Type.Nullable { elem = _elem } 22 | else if (type is reflect.UnknownType) anyType 23 | else if (type is reflect.NothingType) new Type.Declared { typeName = "never" } 24 | else if (type is reflect.StringLiteralType) new Type.Declared { typeName = "\"\(type.value)\"" } 25 | else throw("Unsure how to generate this type: \(type)") 26 | 27 | function generateUnionType( 28 | type: reflect.UnionType, 29 | enclosing: reflect.TypeDeclaration, 30 | seenMappings: List 31 | ): Type = 32 | new Type.Union { 33 | elems = 34 | type.members.map((t) -> generateType(t, enclosing, seenMappings)) 35 | } 36 | 37 | function generateDeclaredType( 38 | type: reflect.DeclaredType, 39 | enclosing: reflect.TypeDeclaration, 40 | seenMappings: List 41 | ): Type = 42 | let (referent = type.referent) 43 | let (reflectee = type.referent.reflectee) 44 | let (mapped = seenMappings.findOrNull((it) -> it.source == referent)) 45 | if (mapped != null) mapped.type 46 | else if (mappedTypes.containsKey(reflectee)) 47 | mappedTypes[reflectee] 48 | else if (referent is reflect.TypeAlias) 49 | generateType(referent.referent, enclosing, seenMappings) 50 | else if (reflectee == List || reflectee == Listing) 51 | generateListing(type, enclosing, seenMappings) 52 | else if (reflectee == Map || reflectee == Mapping) 53 | generateMapping(type, enclosing, seenMappings) 54 | else if (reflectee == Set) 55 | generateSet(type, enclosing, seenMappings) 56 | else if (reflectee == Pair) 57 | generatePair(type, enclosing, seenMappings) 58 | else throw("Cannot generate type \(type.referent.name) as TypeScript.") 59 | 60 | function generateListing( 61 | type: reflect.DeclaredType, 62 | enclosing: reflect.TypeDeclaration, 63 | seenMappings: List 64 | ): Type = 65 | let (typeArg = type.typeArguments.getOrNull(0)) 66 | new Type.Array { 67 | elem = 68 | if (typeArg == null) anyType 69 | else generateType(typeArg, enclosing, seenMappings) 70 | } 71 | 72 | function generateMapping( 73 | type: reflect.DeclaredType, 74 | enclosing: reflect.TypeDeclaration, 75 | seenMappings: List 76 | ): Type = 77 | let (typeArgKey = type.typeArguments.getOrNull(0)) 78 | let (typeArgValue = type.typeArguments.getOrNull(1)) 79 | new Type.Map { 80 | key = 81 | if (typeArgKey == null) anyType 82 | else generateType(typeArgKey, enclosing, seenMappings) 83 | elem = 84 | if (typeArgValue == null) anyType 85 | else generateType(typeArgValue, enclosing, seenMappings) 86 | } 87 | 88 | function generateSet( 89 | type: reflect.DeclaredType, 90 | enclosing: reflect.TypeDeclaration, 91 | seenMappings: List 92 | ): Type = 93 | let (typeArg = type.typeArguments.getOrNull(0)) 94 | new Type.Set { 95 | elem = 96 | if (typeArg == null) anyType 97 | else generateType(typeArg, enclosing, seenMappings) 98 | } 99 | 100 | function generatePair( 101 | type: reflect.DeclaredType, 102 | enclosing: reflect.TypeDeclaration, 103 | seenMappings: List 104 | ): Type = 105 | new Type.Pair { 106 | elems = type.typeArguments.map((t) -> generateType(t, enclosing, seenMappings)) 107 | } 108 | 109 | local anyType: Type.Declared = new Type.Declared { typeName = "pklTypescript.Any" } 110 | 111 | mappedTypes: Mapping = new { 112 | [Int] = new Type.Declared { typeName = "number" } 113 | [Int8] = new Type.Declared { typeName = "number" } 114 | [Int16] = new Type.Declared { typeName = "number" } 115 | [Int32] = new Type.Declared { typeName = "number" } 116 | [UInt] = new Type.Declared { typeName = "number" } 117 | [UInt8] = new Type.Declared { typeName = "number" } 118 | [UInt16] = new Type.Declared { typeName = "number" } 119 | [UInt32] = new Type.Declared { typeName = "number" } 120 | [Number] = new Type.Declared { typeName = "number" } 121 | [Float] = new Type.Declared { typeName = "number" } 122 | [String] = new Type.Declared { typeName = "string" } 123 | [Boolean] = new Type.Declared { typeName = "boolean" } 124 | [Null] = new Type.Declared { typeName = "null" } 125 | [Any] = anyType 126 | [Char] = new Type.Declared { typeName = "string" } 127 | [Duration] = new Type.Declared { 128 | typeName = "pklTypescript.Duration" 129 | } 130 | [DurationUnit] = new Type.Declared { 131 | typeName = "pklTypescript.DurationUnit" 132 | } 133 | [Dynamic] = new Type.Declared { 134 | typeName = "pklTypescript.Dynamic" 135 | } 136 | [DataSize] = new Type.Declared { 137 | typeName = "pklTypescript.DataSize" 138 | } 139 | [DataSizeUnit] = new Type.Declared { 140 | typeName = "pklTypescript.DataSizeUnit" 141 | } 142 | [IntSeq] = new Type.Declared { 143 | typeName = "pklTypescript.IntSeq" 144 | } 145 | [Regex] = new Type.Declared { 146 | typeName = "pklTypescript.Regex" 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/evaluator/evaluator_options.ts: -------------------------------------------------------------------------------- 1 | // EvaluatorOptions is the set of options available to control Pkl evaluation. 2 | import {ProjectOrDependency} from "../types/outgoing"; 3 | import {ModuleReader, ResourceReader} from "./reader"; 4 | import * as path from "path"; 5 | import * as os from "os"; 6 | import {Project} from "./project"; 7 | 8 | export type EvaluatorOptions = { 9 | // properties is the set of properties available to the `prop:` resource reader. 10 | properties?: Record 11 | 12 | // env is the set of environment variables available to the `env:` resource reader. 13 | env?: Record 14 | 15 | // modulePaths is the set of directories, ZIP archives, or JAR archives to search when 16 | // resolving `modulepath`: resources and modules. 17 | // 18 | // This option must be non-emptyMirror if ModuleReaderModulePath or ResourceModulePath are used. 19 | modulePaths?: string[] 20 | 21 | // outputFormat controls the renderer to be used when rendering the `output.text` 22 | // property of a module. 23 | outputFormat?: "json" | "jsonnet" | "pcf" | "plist" | "properties" | "textproto" | "xml" | "yaml" | string 24 | 25 | // allowedModules is the URI patterns that determine which modules can be loaded and evaluated. 26 | allowedModules?: string[] 27 | 28 | // allowedResources is the URI patterns that determine which resources can be loaded and evaluated. 29 | allowedResources?: string[] 30 | 31 | // resourceReaders are the resource readers to be used by the evaluator. 32 | resourceReaders?: ResourceReader[] 33 | 34 | // moduleReaders are the set of custom module readers to be used by the evaluator. 35 | moduleReaders?: ModuleReader[] 36 | 37 | // cacheDir is the directory where `package:` modules are cached. 38 | // 39 | // If empty, no cacheing is performed. 40 | cacheDir?: string 41 | 42 | // rootDir is the root directory for file-based reads within a Pkl program. 43 | // 44 | // Attempting to read past the root directory is an error. 45 | rootDir?: string 46 | 47 | // ProjectDir is the project directory for the evaluator. 48 | // 49 | // Setting this determines how Pkl resolves dependency notation imports. 50 | // It causes Pkl to look for the resolved dependencies relative to this directory, 51 | // and load resolved dependencies from a PklProject.deps.json file inside this directory. 52 | // 53 | // NOTE: 54 | // Setting this option is not equivalent to setting the `--project-dir` flag from the CLI. 55 | // When the `--project-dir` flag is set, the CLI will evaluate the PklProject file, 56 | // and then applies any evaluator settings and dependencies set in the PklProject file 57 | // for the main evaluation. 58 | // 59 | // In contrast, this option only determines how Pkl considers whether files are part of a 60 | // project. 61 | // It is meant to be set by lower level logic in TS that first evaluates the PklProject, 62 | // which then configures EvaluatorOptions accordingly. 63 | // 64 | // To emulate the CLI's `--project-dir` flag, create an evaluator with NewProjectEvaluator, 65 | // or EvaluatorManager.NewProjectEvaluator. 66 | projectDir?: string 67 | 68 | // declaredProjectDependencies is set of dependencies available to modules within ProjectDir. 69 | // 70 | // When importing dependencies, a PklProject.deps.json file must exist within ProjectDir 71 | // that contains the project's resolved dependencies. 72 | declaredProjectDependencies?: ProjectDependencies 73 | } 74 | 75 | export type ProjectDependencies = { 76 | localDependencies: Record 77 | remoteDependencies: Record 78 | } 79 | 80 | export function encodedDependencies(input: ProjectDependencies): Record { 81 | const deps = [...Object.entries(input.localDependencies), ...Object.entries(input.remoteDependencies)] 82 | const depsMessage: [string, ProjectOrDependency][] = deps.map(([key, dep]) => [key, { 83 | packageUri: dep.packageUri, 84 | projectFileUri: "projectFileUri" in dep ? dep.projectFileUri : undefined, 85 | type: "projectFileUri" in dep ? "local" : "remote", 86 | checksums: "projectFileUri" in dep ? undefined : {checksums: dep.checksums.sha256}, 87 | dependencies: "projectFileUri" in dep ? encodedDependencies(dep.dependencies) : undefined, 88 | }]) 89 | 90 | return Object.fromEntries(depsMessage) 91 | } 92 | 93 | type ProjectLocalDependency = { 94 | packageUri: string 95 | 96 | projectFileUri: string 97 | 98 | dependencies: ProjectDependencies 99 | } 100 | 101 | type ProjectRemoteDependency = { 102 | packageUri: string, 103 | checksums: Checksums, 104 | } 105 | 106 | type Checksums = { 107 | sha256: string 108 | } 109 | 110 | export const PreconfiguredOptions: EvaluatorOptions = { 111 | allowedResources: ["http:", "https:", "file:", "env:", "prop:", "modulepath:", "package:", "projectpackage:"], 112 | allowedModules: ["pkl:", "repl:", "file:", "http:", "https:", "modulepath:", "package:", "projectpackage:"], 113 | env: Object.fromEntries(Object.entries(process.env).filter(([k, v]) => v !== undefined)) as Record, 114 | cacheDir: path.join(os.homedir(), ".pkl/cache") 115 | } 116 | 117 | export function withProject(project: Project): EvaluatorOptions { 118 | return {...withProjectEvaluatorSettings(project), ...withProjectDependencies(project)} 119 | } 120 | 121 | function withProjectEvaluatorSettings(project: Project): EvaluatorOptions { 122 | if (project.evaluatorSettings) { 123 | return { 124 | properties: project.evaluatorSettings.externalProperties, 125 | env: project.evaluatorSettings.env, 126 | allowedModules: project.evaluatorSettings.allowedModules, 127 | allowedResources: project.evaluatorSettings.allowedResources, 128 | cacheDir: project.evaluatorSettings.noCache ? undefined : project.evaluatorSettings.moduleCacheDir, 129 | rootDir: project.evaluatorSettings.rootDir, 130 | } 131 | } else { 132 | return {} 133 | } 134 | } 135 | 136 | function withProjectDependencies(project: Project): EvaluatorOptions { 137 | return { 138 | projectDir: project.projectFileUri.replace(/\/PklProject$/, '').replace(/^file:\/\//, ''), 139 | declaredProjectDependencies: project.dependencies 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /codegen/src/internal/utils.pkl: -------------------------------------------------------------------------------- 1 | module pkl.typescript.internal.utils 2 | 3 | import "pkl:reflect" 4 | 5 | local escaper = (c: Char) -> 6 | if (c == "\n") #"\n"# 7 | else if (c == "\"") #"\""# 8 | else if (c == #"\"#) #"\\"# 9 | else c 10 | 11 | /// Turn the Pkl string into a TypeScript string literal. 12 | /// 13 | /// Renders raw string literals if the incoming string is multiline, or contains quotes. 14 | /// 15 | /// Only newlines, double quotes and backslash literals need to be escaped in TypeScript strings. 16 | function toTypescriptString(str: String): String = 17 | if ((str.contains("\n") || str.contains("\"")) && !str.contains("`")) "`" + str + "`" 18 | else "\"" + str.chars.map(escaper).join("") + "\"" 19 | 20 | /// Converts a Pkl declaration (class, property, typealias) into a TypeScript name. 21 | /// If a member has an explicit `@typescript.Name` annotation, use it. 22 | /// 23 | /// Otherwise, normalize the name and return it. 24 | /// 25 | /// Normalization rules: 26 | /// 27 | /// 1. Any non-letter and non-digit characters get stripped, and each proceding letter gets capitalized. 28 | /// 2. If a name does not start with a latin alphabet character, prefix with `N`. 29 | /// 3. Capitalize names so they get exported. 30 | function toTypescriptName(source: reflect.Declaration): String = 31 | source 32 | .annotations 33 | .findOrNull((it) -> it.getClass().toString() == "pkl.typescript.typescript#Name") 34 | ?.value 35 | ?? 36 | // edge case: if the source is the module's companion class, use the module name and not the class name. 37 | let (_name = 38 | if (source is reflect.Class && source.enclosingDeclaration.moduleClass == source) 39 | source.enclosingDeclaration.name.split(".").last 40 | else source.name 41 | ) 42 | normalizeName(_name) 43 | 44 | function toTypescriptPropertyName(source: reflect.Declaration): String = 45 | source 46 | .annotations 47 | .findOrNull((it) -> it.getClass().toString() == "pkl.typescript.typescript#Name") 48 | ?.value 49 | ?? 50 | // edge case: if the source is the module's companion class, use the module name and not the class name. 51 | let (_name = 52 | if (source is reflect.Class && source.enclosingDeclaration.moduleClass == source) 53 | source.enclosingDeclaration.name.split(".").last 54 | else source.name 55 | ) 56 | camelCase(_name) 57 | 58 | /// Sourced from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#keywords 59 | keywords: List = List( 60 | // Reserved words 61 | "break", 62 | "case", 63 | "catch", 64 | "class", 65 | "const", 66 | "continue", 67 | "debugger", 68 | "default", 69 | "delete", 70 | "do", 71 | "else", 72 | "export", 73 | "extends", 74 | "false", 75 | "finally", 76 | "for", 77 | "function", 78 | "if", 79 | "import", 80 | "in", 81 | "instanceof", 82 | "new", 83 | "null", 84 | "return", 85 | "super", 86 | "switch", 87 | "this", 88 | "throw", 89 | "true", 90 | "try", 91 | "typeof", 92 | "var", 93 | "void", 94 | "while", 95 | "with", 96 | 97 | // Reserved in strict mode 98 | "let", 99 | "static", 100 | "yield", 101 | 102 | // Reserved in async code 103 | "await", 104 | 105 | // Future reserved 106 | "enum", 107 | 108 | // Future reserved in strict mode 109 | 110 | "implements", 111 | "interface", 112 | "package", 113 | "private", 114 | "protected", 115 | "public", 116 | 117 | // Future reserved in older EMCAScript standards 118 | "abstract", 119 | "boolean", 120 | "byte", 121 | "char", 122 | "double", 123 | "final", 124 | "float", 125 | "goto", 126 | "int", 127 | "long", 128 | "native", 129 | "short", 130 | "synchronized", 131 | "throws", 132 | "transient", 133 | "volatile", 134 | 135 | 136 | // Identifiers with special meaning 137 | "arguments", 138 | "as", 139 | "async", 140 | "eval", 141 | "from", 142 | "get", 143 | "of", 144 | "set" 145 | ) 146 | 147 | /// As per: https://stackoverflow.com/questions/1661197/what-characters-are-valid-for-javascript-variable-names/9337047#9337047 148 | /// > An identifier must start with $, _, or any character in the Unicode categories “Uppercase letter (Lu)”, “Lowercase letter (Ll)”, “Titlecase letter (Lt)”, “Modifier letter (Lm)”, “Other letter (Lo)”, or “Letter number (Nl)”. 149 | /// > The rest of the string can contain the same characters, plus any U+200C zero width non-joiner characters, U+200D zero width joiner characters, and characters in the Unicode categories “Non-spacing mark (Mn)”, “Spacing combining mark (Mc)”, “Decimal digit number (Nd)”, or “Connector punctuation (Pc)”. 150 | isValidTypescriptName = (it: String) -> 151 | if (keywords.contains(it)) 152 | throw(""" 153 | Name `\(it)` is not valid because it clashes with a JavaScript/TypeScript keyword`. 154 | """) 155 | else it.matches(Regex(#"(?u)\p{L}[\p{L}\d_]+"#)) 156 | 157 | function renderDocComment(docComment: String, indent: String) = 158 | docComment 159 | .split(Regex(#"\r?\n"#)) 160 | .map((it) -> 161 | if (it.trim().isBlank) "\(indent)//" 162 | else "\(indent)// \(it)" 163 | ) 164 | .join("\n") 165 | 166 | 167 | function splitNameOnNonLettersOrDigits(name: String) = 168 | name.split(Regex(#"(?u)[^\p{L}\d]"#)) 169 | 170 | function normalizeName(name: String) = 171 | let (parts = splitNameOnNonLettersOrDigits(name)) 172 | // TODO(Jason): Consider camelCase instead of PascalCase. Types vs variables? 173 | let (pascaled = parts.map((p) -> p.capitalize()).join("")) 174 | if (pascaled[0].matches(Regex(#"[^A-Z]"#))) 175 | "N" + pascaled 176 | else 177 | pascaled 178 | 179 | function camelCase(str: String) = 180 | let (parts = splitNameOnNonLettersOrDigits(str)) 181 | parts.mapIndexed((i, p) -> if (i == 0) p.decapitalize() else p.capitalize()).join("") 182 | 183 | function snakeCase(str: String): String = 184 | let (parts = splitNameOnNonLettersOrDigits(str)) 185 | let (separator = "_") 186 | parts.map((part) -> 187 | part.replaceAllMapped( 188 | Regex("([a-z0-9])([A-Z])"), 189 | (match) -> "\(match.groups[1])\(separator)\(match.groups[2])" 190 | ) 191 | ).join(separator).toLowerCase() 192 | 193 | function renderImports(imports: List): String = 194 | let (distinctImports = imports.distinct) 195 | new Listing { 196 | for (_, mod in distinctImports) { 197 | // Use namespaced imports saves having to detect which values 198 | // should be named-imported or relying on default export. 199 | // This is primarily useful for pklTypescript itself. 200 | "import * as \(camelCase(mod.split("/").last)) from \"\(mod)\"" 201 | } 202 | }.join("\n") 203 | -------------------------------------------------------------------------------- /src/evaluator/decoder.ts: -------------------------------------------------------------------------------- 1 | import * as msgpackr from "msgpackr"; 2 | import { 3 | Any, 4 | AnyObject, 5 | BaseObject, 6 | DataSize, 7 | DataSizeUnit, 8 | Duration, 9 | DurationUnit, 10 | Dynamic, 11 | IntSeq, 12 | Pair, 13 | Regex 14 | } from "../types/pkl"; 15 | 16 | const 17 | codeObject = 0x1 as const, 18 | codeMap = 0x2 as const, 19 | codeMapping = 0x3 as const, 20 | codeList = 0x4 as const, 21 | codeListing = 0x5 as const, 22 | codeSet = 0x6 as const, 23 | codeDuration = 0x7 as const, 24 | codeDataSize = 0x8 as const, 25 | codePair = 0x9 as const, 26 | codeIntSeq = 0xA as const, 27 | codeRegex = 0xB as const, 28 | codeClass = 0xC as const, 29 | codeTypeAlias = 0xD as const, 30 | codeObjectMemberProperty = 0x10 as const, 31 | codeObjectMemberEntry = 0x11 as const, 32 | codeObjectMemberElement = 0x12 as const; 33 | 34 | type code = typeof codeObject | 35 | typeof codeMap | 36 | typeof codeMapping | 37 | typeof codeList | 38 | typeof codeListing | 39 | typeof codeSet | 40 | typeof codeDuration | 41 | typeof codeDataSize | 42 | typeof codePair | 43 | typeof codeIntSeq | 44 | typeof codeRegex | 45 | typeof codeClass | 46 | typeof codeTypeAlias | 47 | typeof codeObjectMemberProperty | 48 | typeof codeObjectMemberEntry | 49 | typeof codeObjectMemberElement; 50 | 51 | type codeObjectMember = typeof codeObjectMemberProperty | 52 | typeof codeObjectMemberEntry | 53 | typeof codeObjectMemberElement; 54 | 55 | 56 | export class Decoder { 57 | readonly decoder: msgpackr.Decoder 58 | 59 | constructor(options: msgpackr.Options) { 60 | this.decoder = new msgpackr.Decoder({ 61 | int64AsType: "bigint", 62 | useRecords: false, 63 | mapsAsObjects: false, 64 | }) 65 | } 66 | 67 | decode(bytes: Uint8Array): Any { 68 | return this.decodeAny(this.decoder.decode(bytes)) 69 | } 70 | 71 | decodeCode(code: code, rest: any[]): AnyObject { 72 | switch (code) { 73 | case codeObject: { 74 | const [name, moduleUri, entries] = rest as [string, string, [codeObjectMember, ...any][]]; 75 | if (name === "Dynamic" && moduleUri === "pkl:base") { 76 | return this.decodeDynamic(entries) 77 | } 78 | return this.decodeObject(name, moduleUri, entries) 79 | } 80 | case codeMap: 81 | case codeMapping: { 82 | const [map] = rest as [Map]; 83 | return this.decodeMap(map) 84 | } 85 | case codeList: 86 | case codeListing: { 87 | const [list] = rest as [any[]]; 88 | return this.decodeList(list) 89 | } 90 | case codeSet: { 91 | const [list] = rest as [any[]]; 92 | return new Set(this.decodeList(list)) 93 | } 94 | case codeDuration: { 95 | const [value, unit] = rest as [number, string]; 96 | 97 | const du: Duration = {value, unit: unit as DurationUnit} 98 | return du 99 | } 100 | case codeDataSize: { 101 | const [value, unit] = rest as [number, string]; 102 | 103 | const ds: DataSize = {value, unit: unit as DataSizeUnit} 104 | return ds 105 | } 106 | case codePair: { 107 | const [first, second] = rest as [any, any] 108 | const p: Pair = [this.decodeAny(first), this.decodeAny(second)] 109 | return p 110 | } 111 | case codeIntSeq: { 112 | const [start, end, step] = rest as [number, number, number]; 113 | 114 | const is: IntSeq = {start, end, step} 115 | return is 116 | } 117 | case codeRegex: { 118 | const [pattern] = rest as [string]; 119 | 120 | const re: Regex = {pattern} 121 | return re 122 | } 123 | case codeClass: { 124 | return {} 125 | } 126 | case codeTypeAlias: { 127 | return {} 128 | } 129 | default: { 130 | throw new Error(`encountered unknown object code: ${code}`) 131 | } 132 | } 133 | } 134 | 135 | decodeObject(name: string, moduleUri: string, rest: [codeObjectMember, ...any][]): BaseObject { 136 | const out: BaseObject = {} 137 | 138 | for (const entry of rest) { 139 | const [code, ...rest] = entry; 140 | switch (code) { 141 | case codeObjectMemberProperty: { 142 | const [name, value] = rest as [string, any] 143 | out[name] = this.decodeAny(value) 144 | break 145 | } 146 | case codeObjectMemberEntry: { 147 | throw new Error("Unexpected object member entry in non-Dynamic object") 148 | } 149 | case codeObjectMemberElement: { 150 | throw new Error("Unexpected object member element in non-Dynamic object") 151 | } 152 | } 153 | } 154 | 155 | return out 156 | } 157 | 158 | decodeDynamic(rest: [codeObjectMember, ...any][]): Dynamic { 159 | let properties: Record = {} 160 | let entries = new Map 161 | let elements = new Array 162 | 163 | for (const entry of rest) { 164 | const [code, ...rest] = entry; 165 | switch (code) { 166 | case codeObjectMemberProperty: { 167 | const [name, value] = rest as [any, any] 168 | if (typeof name !== "string") { 169 | throw new Error("object member property keys must be strings") 170 | } 171 | properties[name] = this.decodeAny(value) 172 | break 173 | } 174 | case codeObjectMemberEntry: { 175 | const [key, value] = rest as [any, any] 176 | entries.set(this.decodeAny(key), this.decodeAny(value)) 177 | break 178 | } 179 | case codeObjectMemberElement: { 180 | const [i, value] = rest as [any, any] 181 | if (typeof i !== "number") { 182 | throw new Error("object member element indices must be numbers") 183 | } 184 | elements[i] = this.decodeAny(value) 185 | break 186 | } 187 | } 188 | } 189 | 190 | return {properties, entries, elements} 191 | } 192 | 193 | decodeMap(map: Map): Map { 194 | const out = new Map(); 195 | 196 | for (const [k, v] of map.entries()) { 197 | out.set(this.decodeAny(k), this.decodeAny(v)) 198 | } 199 | 200 | return out 201 | } 202 | 203 | decodeList(list: any[]): Any[] { 204 | return list.map((item) => this.decodeAny(item)) 205 | } 206 | 207 | decodeAny(value: any): Any { 208 | if (value === null) { 209 | return value 210 | } 211 | if (Array.isArray(value)) { 212 | // object case 213 | const [code, ...rest] = value 214 | return this.decodeCode(code, rest) 215 | } 216 | if (typeof value === "object") { 217 | throw new Error(`unexpected object ${value} provided to decodeAny; expected primitive type or Array`) 218 | } 219 | // primitives 220 | return value 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!CAUTION] 2 | > 3 | > # THIS LIBRARY IS CURRENTLY PRE-RELEASE 4 | > 5 | > `pkl-typescript` is currently major version `v0`, and **breaking changes will happen** between versions. 6 | > 7 | > Please read the section [Roadmap](#roadmap) below to learn more. 8 | 9 | # Pkl Bindings for TypeScript 10 | 11 | This library exposes TypeScript language bindings for the Pkl configuration language. 12 | 13 | These language bindings are made up of: 14 | 15 | - an "evaluator", that can execute Pkl code and deserialise the result into JavaScript runtime objects 16 | - the `pkl-gen-typescript` CLI, that can analyse a Pkl schema and generate matching TypeScript definitions 17 | 18 | Together, this allows you to embed Pkl into your TypeScript application, complete with code generation for full type safety and ease of use. 19 | 20 | ## Getting Started 21 | 22 | First, install `pkl-typescript` from NPM: 23 | 24 | ```bash 25 | npm install @pkl-community/pkl-typescript 26 | ``` 27 | 28 | Then, generate a TypeScript file from your Pkl schema (eg. for a file called "config.pkl"): 29 | 30 | ```bash 31 | npx pkl-gen-typescript config.pkl -o ./generated 32 | ``` 33 | 34 | Lastly, in your TypeScript code, you can import the generated types and loader code: 35 | 36 | ```typescript 37 | import { type Config, loadFromPath } from "./generated/config.pkl.ts"; 38 | 39 | const config: Config = await loadFromPath("config.pkl"); 40 | ``` 41 | 42 | See more example usage in the [examples directory](./examples/). 43 | 44 | ### Note on Schemas vs. Configs 45 | 46 | `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`. 47 | Conversely, the evaluator (used by the `loadFromPath(string)` function) evaluates a Pkl module that **renders values**. 48 | 49 | 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")`). 50 | 51 | ## Roadmap 52 | 53 | 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. 54 | 55 | We will keep the major version at `v0` until we are ready to commit to stability in: 56 | 57 | - the evaluator API (as provided by the `@pkl-community/pkl-typescript` NPM package) 58 | - the TypeScript type definitions generated by `pkl-gen-typescript` 59 | 60 | Until then, minor and patch releases may contain breaking changes. 61 | 62 | > [!WARNING] 63 | > **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. 64 | 65 | ### Known Current Limitations 66 | 67 | - **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. 68 | - **Subclass type overrides**: Pkl class definitions are generated as TypeScript interfaces in code generation; Pkl supports completely changing the type of a property in a child class, but this is not allowed in TypeScript extending interfaces. When a TypeScript interface `extends` a parent interface, overrides of the type of a property must be "compatible" with the parent type (eg. overriding a `string` type with a string-literal type). TypeScript codegen currently has support for a few compatible types, and others may be allowed in the future (if you have an example of a compatible type that should work but fails in codegen, please file a GitHub Issue). 69 | - **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)`. 70 | - **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. 71 | - **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. 72 | 73 | ## Appendix 74 | 75 | ### Pkl Binary Version 76 | 77 | This package has a peer dependency on `@pkl-community/pkl`, to ensure a Pkl binary is installed. You can use an alternative Pkl binary (for either the evaluator or codegen) by setting the environment variable `PKL_EXEC` with the path to a Pkl binary. 78 | 79 | ### Type Mappings 80 | 81 | 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! 82 | 83 | | Pkl type | TypeScript type | 84 | | ---------------- | -------------------------- | 85 | | Null | `null` | 86 | | Boolean | `boolean` | 87 | | String | `string` | 88 | | Int | `number` | 89 | | Int8 | `number` | 90 | | Int16 | `number` | 91 | | Int32 | `number` | 92 | | UInt | `number` | 93 | | UInt8 | `number` | 94 | | UInt16 | `number` | 95 | | UInt32 | `number` | 96 | | Float | `number` | 97 | | Number | `number` | 98 | | List | `Array` | 99 | | Listing | `Array` | 100 | | Map | `Map` | 101 | | Mapping | `Map` | 102 | | Set | `Set` | 103 | | Pair | `pklTypescript.Pair` | 104 | | Dynamic | `pklTypescript.Dynamic` | 105 | | DataSize | `pklTypescript.DataSize` | 106 | | Duration | `pklTypescript.Duration` | 107 | | IntSeq | `pklTypescript.IntSeq` | 108 | | Class | `interface` | 109 | | TypeAlias | `typealias` | 110 | | Any | `pklTypescript.Any` | 111 | | Unions (A\|B\|C) | `A\|B\|C` | 112 | | Regex | `pklTypescript.Regex` | 113 | -------------------------------------------------------------------------------- /codegen/src/internal/ClassGen.pkl: -------------------------------------------------------------------------------- 1 | module pkl.typescript.internal.ClassGen 2 | 3 | extends "Gen.pkl" 4 | 5 | import "pkl:reflect" 6 | import "TypescriptMapping.pkl" 7 | import "utils.pkl" 8 | import "Type.pkl" 9 | import "typegen.pkl" 10 | 11 | clazz: reflect.Class = mapping.source as reflect.Class 12 | 13 | classInfo: TypescriptMapping.Class = mapping as TypescriptMapping.Class 14 | 15 | local isModule: Boolean = clazz.enclosingDeclaration.moduleClass == clazz 16 | 17 | contents = new Listing { 18 | when (interface != null) { 19 | if (isModule) "// Ref: Module root." else "// Ref: Pkl class `\(clazz.enclosingDeclaration.name).\(clazz.name)`." 20 | 21 | interface 22 | } 23 | }.join("\n") 24 | 25 | local isSuperOpen: Boolean = clazz.superclass.modifiers.contains("open") 26 | 27 | local isAbstract: Boolean = clazz.modifiers.contains("abstract") 28 | 29 | local superClass: TypescriptMapping.Class? = mappings.findOrNull((c) -> c is TypescriptMapping.Class && c.clazz == clazz.superclass) as TypescriptMapping.Class? 30 | 31 | local fields: Map = getFields(clazz, mappings) 32 | 33 | imports = 34 | fields.values 35 | .flatMap((f) -> f.type.imports) 36 | .filter((i) -> i != classInfo.typescriptModule).distinct 37 | + (if (superClass != null && superClass.typescriptModule != classInfo.typescriptModule) List(superClass.typescriptModule) else List()) 38 | + (if (isModule && !isAbstract) 39 | List("@pkl-community/pkl-typescript") 40 | else List()) 41 | 42 | local function getAllProperties(clazz: reflect.Class?): List = 43 | if (clazz == null) List() 44 | else if (doesNotInherit(clazz)) clazz.properties.values 45 | else clazz.properties.values + getAllProperties(clazz.superclass!!) 46 | 47 | local function isSameType(typeA: reflect.Type, typeB: reflect.Type) = 48 | if (typeA is reflect.DeclaredType && typeB is reflect.DeclaredType) 49 | typeA.referent.reflectee == typeB.referent.reflectee && 50 | typeA.typeArguments.length == typeB.typeArguments.length && 51 | typeA.typeArguments 52 | .zip(typeB.typeArguments) 53 | .every((pair) -> isSameType(pair.first, pair.second)) 54 | else if (typeA is reflect.NullableType && typeB is reflect.NullableType) 55 | isSameType(typeA.member, typeB.member) 56 | else if (typeA is reflect.NothingType && typeB is reflect.NothingType) 57 | true 58 | else if (typeA is reflect.UnknownType && typeB is reflect.UnknownType) 59 | true 60 | else if (typeA is reflect.StringLiteralType && typeB is reflect.StringLiteralType) 61 | typeA.value == typeB.value 62 | else if (typeA is reflect.UnionType && typeB is reflect.UnionType) 63 | typeA.members.length == typeB.members.length && 64 | typeA.members.sortBy((m) -> m.referent.reflectee.toString()) 65 | .zip(typeB.members.sortBy((m) -> m.referent.reflectee.toString())) 66 | .every((pair) -> isSameType(pair.first, pair.second)) 67 | // remaining types: `FunctionType`, `TypeParameter`, `ModuleType`. 68 | // we can actually check if `ModuleType` refers to the same type by checking if the enclosing declaration is the same, 69 | // but we will pretend it is always false for now. 70 | else false 71 | 72 | // TypeScript allows extending interfaces to override properties from the parent interface, as long 73 | // as the new type is compatible with the parent property's type. This check is not exhaustive and 74 | // will likely be updated over time. 75 | local function isCompatibleType(parentType: reflect.Type, childType: reflect.Type) = 76 | ( 77 | parentType is reflect.DeclaredType && 78 | ( 79 | ( 80 | // String type can be overridden by string literal type 81 | parentType == reflect.stringType && 82 | childType is reflect.StringLiteralType 83 | ) || 84 | ( 85 | // Same type, different but compatible type arguments 86 | childType is reflect.DeclaredType && 87 | parentType.referent.reflectee == childType.referent.reflectee && 88 | ( 89 | parentType.typeArguments 90 | .zip(childType.typeArguments) 91 | .every((pair) -> 92 | isSameType(pair.first, pair.second) || 93 | isCompatibleType(pair.first, pair.second) 94 | ) 95 | ) 96 | ) 97 | ) 98 | ) 99 | || 100 | ( 101 | parentType is reflect.UnionType && 102 | ( 103 | ( 104 | // Child union can be a subset of the parent union's members 105 | childType is reflect.UnionType && 106 | childType.members.every((m) -> parentType.members.contains(m)) 107 | ) 108 | // Or child type can be one of the types from the parent union 109 | || parentType.members.contains(childType) 110 | ) 111 | ) 112 | 113 | // visible for testing 114 | function getFields( 115 | clazz: reflect.Class, 116 | mappings: List 117 | ): Map = 118 | let (isSuperOpen: Boolean = clazz.superclass.modifiers.contains("open")) 119 | let (superProperties = getAllProperties(clazz.superclass)) 120 | clazz.properties 121 | .filter((propName, prop: reflect.Property) -> 122 | let (superProp = superProperties.findOrNull((it) -> it.name == prop.name)) 123 | // don't render hidden members or functions 124 | if (prop.modifiers.contains("hidden") || prop.type is reflect.FunctionType) false 125 | // Okay if there is no property override, or if the super property has the same type. 126 | else if (superProp == null || isSameType(superProp.type, prop.type) || isCompatibleType(superProp.type, prop.type)) true 127 | // Okay if the property is overridden but does not define a type, but don't render as its own field. 128 | // E.g. `class Foo extends Bar { bar = "mybar" }` 129 | else if (prop.type is reflect.UnknownType) !isSuperOpen 130 | // Otherwise, the property's type has been overridden, and this is currently 131 | // not supported - would require something like `extends Omit` 132 | else throw(""" 133 | Illegal: Class `\(clazz.reflectee)` overrides property `\(propName)`. This is only supported by TypeScript when the new type is compatible with the parent type. 134 | 135 | \(prop.location.displayUri) 136 | """) 137 | ) 138 | .mapValues((_, prop: reflect.Property) -> 139 | new TypescriptInterfaceProperty { 140 | isInherited = false 141 | type = typegen.generateType(prop.type, clazz, mappings) 142 | docComment = prop.docComment 143 | name = utils.toTypescriptPropertyName(prop) 144 | property = prop 145 | } 146 | ) 147 | 148 | local function doesNotInherit(clazz: reflect.Class?) = 149 | clazz.superclass == null || clazz.superclass.reflectee == Module || clazz.superclass.reflectee == Typed 150 | 151 | // local methodsToGenerate = fields.filter((_, field) -> !field.isInherited) 152 | local interface: String = new Listing { 153 | when (clazz.docComment != null) { 154 | utils.renderDocComment(clazz.docComment!!, "") 155 | } 156 | ( 157 | "export interface \(classInfo.interface.name) " + 158 | (if (superClass != null) 159 | "extends " + superClass.type.render(classInfo.typescriptModule) + " " 160 | else "") 161 | + "{" 162 | ) 163 | 164 | for (pklPropertyName, field in fields) { 165 | when (pklPropertyName != fields.keys.first) { 166 | "" 167 | } 168 | when (field.docComment != null) { 169 | utils.renderDocComment(field.docComment!!, " ") 170 | } 171 | renderInterfaceProperty(pklPropertyName, field) 172 | } 173 | "}" 174 | } 175 | .join("\n") 176 | 177 | local function renderInterfaceProperty(pklPropertyName: String, field: TypescriptInterfaceProperty): String = 178 | new Listing { 179 | " " 180 | field.name 181 | ": " 182 | field.type.render(classInfo.typescriptModule) 183 | }.join("") 184 | 185 | local class TypescriptInterfaceProperty { 186 | /// Is this field inherited from a parent? 187 | isInherited: Boolean 188 | 189 | /// The name of the field 190 | name: String 191 | 192 | /// The Go type associated with this field 193 | type: Type 194 | 195 | /// The doc comments on the field 196 | docComment: String? 197 | 198 | /// The Pkl property behind the field 199 | property: reflect.Property 200 | } 201 | -------------------------------------------------------------------------------- /src/evaluator/evaluator.ts: -------------------------------------------------------------------------------- 1 | import {ModuleSource} from "./module_source"; 2 | import {EvaluatorManager} from "./evaluator_manager"; 3 | import {EvaluateResponse, ListModules, ListResources, Log, ReadModule, ReadResource} from "../types/incoming"; 4 | import { 5 | codeEvaluate, 6 | codeEvaluateReadModuleResponse, 7 | codeEvaluateReadResponse, 8 | codeListModulesResponse, 9 | codeListResourcesResponse 10 | } from "../types/codes"; 11 | import {ModuleReader, ResourceReader} from "./reader"; 12 | import {Evaluate} from "../types/outgoing"; 13 | import {Any} from "../types/pkl"; 14 | 15 | 16 | // Evaluator is an interface for evaluating Pkl modules. 17 | export interface Evaluator { 18 | // evaluateModule evaluates the given module, and writes it to the value pointed by 19 | // out. 20 | // 21 | // This method is designed to work with TS modules that have been code generated from Pkl 22 | // sources. 23 | evaluateModule(source: ModuleSource): Promise 24 | 25 | // evaluateOutputText evaluates the `output.text` property of the given module. 26 | evaluateOutputText(source: ModuleSource): Promise 27 | 28 | // evaluateOutputValue evaluates the `output.value` property of the given module, 29 | // and writes to the value pointed by out. 30 | evaluateOutputValue(source: ModuleSource): Promise 31 | 32 | // evaluateOutputFiles evaluates the `output.files` property of the given module. 33 | evaluateOutputFiles(source: ModuleSource): Promise> 34 | 35 | // evaluateExpression evaluates the provided expression on the given module source, and writes 36 | // the result into the value pointed by out. 37 | evaluateExpression(source: ModuleSource, expr: string): Promise 38 | 39 | // evaluateExpressionRaw evaluates the provided module, and returns the underlying value's raw 40 | // bytes. 41 | // 42 | // This is a low level API. 43 | evaluateExpressionRaw(source: ModuleSource, expr: string): Promise 44 | 45 | // close closes the evaluator and releases any underlying resources. 46 | close(): void 47 | 48 | // closed tells if this evaluator is closed. 49 | closed: boolean 50 | } 51 | 52 | export class EvaluatorImpl implements Evaluator { 53 | closed: boolean = false; 54 | pendingRequests: Map void, reject: (err: any) => void }> 55 | resourceReaders: ResourceReader[] = [] 56 | moduleReaders: ModuleReader[] = [] 57 | randState: bigint; 58 | 59 | constructor(private evaluatorId: bigint, private manager: EvaluatorManager) { 60 | this.pendingRequests = new Map() 61 | this.randState = evaluatorId 62 | } 63 | 64 | close(): void { 65 | this.manager.close() 66 | } 67 | 68 | async evaluateExpression(source: ModuleSource, expr: string): Promise { 69 | const bytes = await this.evaluateExpressionRaw(source, expr) 70 | return this.manager.decoder.decode(bytes) 71 | } 72 | 73 | async evaluateExpressionRaw(source: ModuleSource, expr: string): Promise { 74 | if (this.closed) { 75 | throw new Error("evaluator is closed") 76 | } 77 | 78 | let evaluate: Evaluate = { 79 | requestId: this.randomInt63(), 80 | evaluatorId: this.evaluatorId, 81 | moduleUri: source.uri.toString(), 82 | code: codeEvaluate, 83 | } 84 | 85 | if (expr) evaluate.expr = expr; 86 | if (source.contents) evaluate.moduleText = source.contents 87 | 88 | 89 | const responsePromise = new Promise((resolve, reject) => { 90 | this.pendingRequests.set(evaluate.requestId.toString(), {resolve, reject}) 91 | }).finally(() => this.pendingRequests.delete(evaluate.requestId.toString())) 92 | 93 | await this.manager.send(evaluate) 94 | 95 | const resp = await responsePromise 96 | if (resp.error) { 97 | throw new Error(resp.error) 98 | } 99 | 100 | return resp.result 101 | } 102 | 103 | evaluateModule(source: ModuleSource): Promise { 104 | return this.evaluateExpression(source, "") 105 | } 106 | 107 | async evaluateOutputFiles(source: ModuleSource): Promise> { 108 | return await this.evaluateExpression(source, "output.files.toMap().mapValues((_, it) -> it.text)") as Record 109 | } 110 | 111 | async evaluateOutputText(source: ModuleSource): Promise { 112 | return await this.evaluateExpression(source, "output.text") as string 113 | } 114 | 115 | evaluateOutputValue(source: ModuleSource): Promise { 116 | return this.evaluateExpression(source, "output.value") 117 | } 118 | 119 | handleEvaluateResponse(msg: EvaluateResponse) { 120 | const pending = this.pendingRequests.get(msg.requestId.toString()) 121 | if (!pending) { 122 | console.error("warn: received a message for an unknown request id:", msg.requestId) 123 | return 124 | } 125 | pending.resolve(msg) 126 | } 127 | 128 | handleLog(resp: Log) { 129 | switch (resp.level) { 130 | case 0: 131 | console.trace(resp.message, resp.frameUri) 132 | break 133 | case 1: 134 | console.warn(resp.message, resp.frameUri) 135 | break 136 | default: 137 | // log level beyond 1 is impossible 138 | throw new Error(`unknown log level: ${resp.level}`) 139 | } 140 | } 141 | 142 | async handleReadResource(msg: ReadResource) { 143 | const response = {evaluatorId: this.evaluatorId, requestId: msg.requestId, code: codeEvaluateReadResponse} 144 | let url: URL; 145 | try { 146 | url = new URL(msg.uri) 147 | } catch (e) { 148 | await this.manager.send({...response, error: `internal error: failed to parse resource url: ${e}`}) 149 | return 150 | } 151 | 152 | const reader = this.resourceReaders.find((r) => `${r.scheme}:` === url.protocol) 153 | 154 | if (!reader) { 155 | await this.manager.send({...response, error: `No resource reader found for scheme ${url.protocol}`}) 156 | return 157 | } 158 | 159 | try { 160 | const contents = reader.read(url) 161 | await this.manager.send({...response, contents}) 162 | } catch (e) { 163 | await this.manager.send({...response, error: `${e}`}) 164 | } 165 | } 166 | 167 | async handleReadModule(msg: ReadModule) { 168 | const response = {evaluatorId: this.evaluatorId, requestId: msg.requestId, code: codeEvaluateReadModuleResponse} 169 | let url: URL; 170 | try { 171 | url = new URL(msg.uri) 172 | } catch (e) { 173 | await this.manager.send({...response, error: `internal error: failed to parse resource url: ${e}`}) 174 | return 175 | } 176 | 177 | const reader = this.moduleReaders.find((r) => `${r.scheme}:` === url.protocol) 178 | 179 | if (!reader) { 180 | await this.manager.send({...response, error: `No module reader found for scheme ${url.protocol}`}) 181 | return 182 | } 183 | 184 | try { 185 | const contents = reader.read(url) 186 | await this.manager.send({...response, contents}) 187 | } catch (e) { 188 | await this.manager.send({...response, error: `${e}`}) 189 | } 190 | } 191 | 192 | async handleListResources(msg: ListResources) { 193 | const response = {evaluatorId: this.evaluatorId, requestId: msg.requestId, code: codeListResourcesResponse} 194 | let url: URL; 195 | try { 196 | url = new URL(msg.uri) 197 | } catch (e) { 198 | await this.manager.send({...response, error: `internal error: failed to parse resource url: ${e}`}) 199 | return 200 | } 201 | 202 | const reader = this.resourceReaders.find((r) => `${r.scheme}:` === url.protocol) 203 | 204 | if (!reader) { 205 | await this.manager.send({...response, error: `No resource reader found for scheme ${url.protocol}`}) 206 | return 207 | } 208 | 209 | try { 210 | const pathElements = reader.listElements(url) 211 | await this.manager.send({...response, pathElements}) 212 | } catch (e) { 213 | await this.manager.send({...response, error: `${e}`}) 214 | } 215 | } 216 | 217 | async handleListModules(msg: ListModules) { 218 | const response = {evaluatorId: this.evaluatorId, requestId: msg.requestId, code: codeListModulesResponse} 219 | let url: URL; 220 | try { 221 | url = new URL(msg.uri) 222 | } catch (e) { 223 | await this.manager.send({...response, error: `internal error: failed to parse resource url: ${e}`}) 224 | return 225 | } 226 | 227 | const reader = this.resourceReaders.find((r) => `${r.scheme}:` === url.protocol) 228 | 229 | if (!reader) { 230 | await this.manager.send({...response, error: `No module reader found for scheme ${url.protocol}`}) 231 | return 232 | } 233 | 234 | try { 235 | const pathElements = reader.listElements(url) 236 | await this.manager.send({...response, pathElements}) 237 | } catch (e) { 238 | await this.manager.send({...response, error: `${e}`}) 239 | } 240 | } 241 | 242 | private static U64_MASK = ((1n << 64n) - 1n) 243 | private static U63_MASK = ((1n << 63n) - 1n) 244 | private randomInt63(): bigint { 245 | this.randState = (this.randState + 0x9e3779b97f4a7c15n) & EvaluatorImpl.U64_MASK; 246 | let next: bigint = this.randState; 247 | next = ((next ^ (next >> 30n)) * 0xbf58476d1ce4e5b9n) & EvaluatorImpl.U64_MASK; 248 | next = ((next ^ (next >> 27n)) * 0x94d049bb133111ebn) & EvaluatorImpl.U64_MASK; 249 | next = next ^ (next >> 31n); 250 | return next & EvaluatorImpl.U63_MASK 251 | } 252 | } 253 | 254 | -------------------------------------------------------------------------------- /src/evaluator/evaluator_manager.ts: -------------------------------------------------------------------------------- 1 | import {EvaluatorImpl, Evaluator} from "./evaluator"; 2 | import {CreateEvaluator, OutgoingMessage, packMessage} from "../types/outgoing"; 3 | import * as msgpackr from "msgpackr"; 4 | import { 5 | codeEvaluateLog, 6 | codeEvaluateRead, 7 | codeEvaluateReadModule, 8 | codeEvaluateResponse, 9 | codeListModulesRequest, 10 | codeListResourcesRequest, 11 | codeNewEvaluator, 12 | codeNewEvaluatorResponse 13 | } from "../types/codes"; 14 | import {encodedDependencies, EvaluatorOptions, PreconfiguredOptions, withProject} from "./evaluator_options"; 15 | import {loadProjectFromEvaluator, Project} from "./project"; 16 | import {spawn, spawnSync} from "node:child_process"; 17 | import {Readable, Writable} from "node:stream"; 18 | import {ChildProcessByStdio} from "child_process"; 19 | import {CreateEvaluatorResponse, decode, IncomingMessage} from "../types/incoming"; 20 | import {Decoder} from "./decoder"; 21 | 22 | // newEvaluatorManager creates a new EvaluatorManager. 23 | export function newEvaluatorManager(): EvaluatorManagerInterface { 24 | return newEvaluatorManagerWithCommand([]) 25 | } 26 | 27 | // newEvaluatorManagerWithCommand creates a new EvaluatorManager using the given pkl command. 28 | // 29 | // The first element in pklCmd is treated as the command to run. 30 | // Any additional elements are treated as arguments to be passed to the process. 31 | // pklCmd is treated as the base command that spawns Pkl. 32 | // For example, the below snippet spawns the command /opt/bin/pkl. 33 | // 34 | // newEvaluatorManagerWithCommand(["/opt/bin/pkl"]) 35 | export function newEvaluatorManagerWithCommand(pklCommand: string[]): EvaluatorManagerInterface { 36 | return new EvaluatorManager(pklCommand) 37 | } 38 | 39 | 40 | export interface EvaluatorManagerInterface { 41 | // close closes the evaluator manager and all of its evaluators. 42 | // 43 | // If running Pkl as a child process, closes all evaluators as well as the child process. 44 | // If calling into Pkl through the C API, close all existing evaluators. 45 | close(): void 46 | 47 | getVersion(): string 48 | 49 | // newEvaluator constructs an evaluator instance. 50 | // 51 | // If calling into Pkl as a child process, the first time NewEvaluator is called, this will 52 | // start the child process. 53 | newEvaluator(opts: EvaluatorOptions): Promise 54 | 55 | // newProjectEvaluator is an easy way to create an evaluator that is configured by the specified 56 | // projectDir. 57 | // 58 | // It is similar to running the `pkl eval` or `pkl test` CLI command with a set `--project-dir`. 59 | // 60 | // When using project dependencies, they must first be resolved using the `pkl project resolve` 61 | // CLI command. 62 | newProjectEvaluator(projectDir: string, opts: EvaluatorOptions): Promise 63 | } 64 | 65 | 66 | export class EvaluatorManager implements EvaluatorManagerInterface { 67 | private static semverPattern = /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?/ 68 | private static pklVersionRegex = new RegExp(`Pkl (${this.semverPattern.toString()}).*`) 69 | 70 | private pendingEvaluators: Map void, 72 | reject: (err: any) => void 73 | }> = new Map() 74 | private evaluators: Map = new Map() 75 | private closed: boolean = false 76 | private version?: string 77 | private cmd: ChildProcessByStdio; 78 | private readonly msgpackConfig: msgpackr.Options = {int64AsType: 'bigint', useRecords: false, encodeUndefinedAsNil: true} 79 | private readonly encoder: msgpackr.Encoder = new msgpackr.Encoder(this.msgpackConfig); 80 | readonly decoder: Decoder = new Decoder(this.msgpackConfig); 81 | private streamDecoder: msgpackr.UnpackrStream = new msgpackr.UnpackrStream(this.msgpackConfig) 82 | 83 | constructor(private readonly pklCommand: string[]) { 84 | const [cmd, args] = this.getStartCommand(); 85 | this.cmd = spawn(cmd, args, { 86 | env: process.env, 87 | stdio: ['pipe', 'pipe', 'inherit'] 88 | }) 89 | 90 | this.decode(this.cmd.stdout).catch(console.error) 91 | this.cmd.on('close', () => { 92 | this.pendingEvaluators.forEach(({reject}) => { 93 | reject(new Error("pkl command exited")) 94 | }) 95 | let errors: any[] = []; 96 | this.evaluators.forEach((ev) => { 97 | try { 98 | ev.close() 99 | } catch (e) { 100 | errors.push(e) 101 | } 102 | }) 103 | this.closed = true 104 | if (errors.length > 0) { 105 | console.error("errors closing evaluators:", errors) 106 | } 107 | }) 108 | } 109 | 110 | private getCommandAndArgStrings(): [string, string[]] { 111 | if (this.pklCommand.length > 0) { 112 | return [this.pklCommand[0], this.pklCommand.slice(1)] 113 | } 114 | const pklExecEnv = process.env["PKL_EXEC"] ?? "" 115 | if (pklExecEnv != "") { 116 | const parts = pklExecEnv.split(" ") 117 | return [parts[0], parts.slice(1)] 118 | } 119 | return ["pkl", []] 120 | } 121 | 122 | async send(out: OutgoingMessage) { 123 | await new Promise((resolve, reject) => this.cmd.stdin.write(packMessage(this.encoder, out), (error) => { 124 | if (error) { 125 | reject(error) 126 | } else { 127 | resolve() 128 | } 129 | })) 130 | } 131 | 132 | private getEvaluator(evaluatorId: bigint): EvaluatorImpl | undefined { 133 | const ev = this.evaluators.get(evaluatorId) 134 | if (!ev) { 135 | console.log("Received unknown evaluator id:", evaluatorId) 136 | return undefined 137 | } 138 | return ev 139 | } 140 | 141 | private async decode(stdout: Readable) { 142 | stdout.pipe(this.streamDecoder) 143 | for await (const item of this.streamDecoder) { 144 | const decoded = decode(item); 145 | let ev: EvaluatorImpl | undefined; 146 | switch (decoded.code) { 147 | case codeEvaluateResponse: 148 | ev = this.getEvaluator(decoded.evaluatorId) 149 | if (!ev) { 150 | continue 151 | } 152 | ev.handleEvaluateResponse(decoded) 153 | continue 154 | case codeEvaluateLog: 155 | ev = this.getEvaluator(decoded.evaluatorId) 156 | if (!ev) { 157 | continue 158 | } 159 | ev.handleLog(decoded) 160 | continue 161 | case codeEvaluateRead: 162 | ev = this.getEvaluator(decoded.evaluatorId) 163 | if (!ev) { 164 | continue 165 | } 166 | await ev.handleReadResource(decoded) 167 | continue 168 | case codeEvaluateReadModule: 169 | ev = this.getEvaluator(decoded.evaluatorId) 170 | if (!ev) { 171 | continue 172 | } 173 | await ev.handleReadModule(decoded) 174 | continue 175 | case codeListResourcesRequest: 176 | ev = this.getEvaluator(decoded.evaluatorId) 177 | if (!ev) { 178 | continue 179 | } 180 | await ev.handleListResources(decoded) 181 | continue 182 | case codeListModulesRequest: 183 | ev = this.getEvaluator(decoded.evaluatorId) 184 | if (!ev) { 185 | continue 186 | } 187 | await ev.handleListModules(decoded) 188 | continue 189 | case codeNewEvaluatorResponse: 190 | const pending = this.pendingEvaluators.get(decoded.requestId.toString()) 191 | if (!pending) { 192 | console.error("warn: received a message for an unknown request id:", decoded.requestId) 193 | return 194 | } 195 | pending.resolve(decoded) 196 | } 197 | } 198 | } 199 | 200 | private getStartCommand(): [string, string[]] { 201 | const [cmd, args] = this.getCommandAndArgStrings() 202 | return [cmd, [...args, "server"]] 203 | } 204 | 205 | 206 | close(): void { 207 | this.cmd.kill() 208 | } 209 | 210 | getVersion(): string { 211 | if (this.version) { 212 | return this.version 213 | } 214 | const [cmd, args] = this.getCommandAndArgStrings() 215 | const result = spawnSync(cmd, [...args, "--version"]) 216 | const version = result.stdout.toString().match(EvaluatorManager.pklVersionRegex) 217 | 218 | if (!version?.length || version.length < 2) { 219 | throw new Error(`failed to get version information from Pkl. Ran '${args.join(" ")}', and got stdout "${result.stdout.toString()}"`) 220 | } 221 | this.version = version[1] 222 | return this.version 223 | } 224 | 225 | async newEvaluator(opts: EvaluatorOptions): Promise { 226 | if (this.closed) { 227 | throw new Error("EvaluatorManager has been closed") 228 | } 229 | 230 | const createEvaluator: CreateEvaluator = { 231 | requestId: BigInt(0), // TODO 232 | clientResourceReaders: opts.resourceReaders ?? [], 233 | clientModuleReaders: opts.moduleReaders ?? [], 234 | code: codeNewEvaluator, 235 | ...opts, 236 | } 237 | 238 | if (opts.projectDir) createEvaluator.project = { 239 | projectFileUri: `file://${opts.projectDir}/PklProject`, 240 | dependencies: opts.declaredProjectDependencies ? encodedDependencies(opts.declaredProjectDependencies) : undefined 241 | } 242 | 243 | const responsePromise = new Promise((resolve, reject) => { 244 | this.pendingEvaluators.set(createEvaluator.requestId.toString(), {resolve, reject}) 245 | }).finally(() => this.pendingEvaluators.delete(createEvaluator.requestId.toString())) 246 | 247 | await this.send(createEvaluator) 248 | 249 | const response = await responsePromise; 250 | if (response.error && response.error !== "") { 251 | throw new Error(response.error) 252 | } 253 | const ev = new EvaluatorImpl(response.evaluatorId, this); 254 | this.evaluators.set(response.evaluatorId, ev) 255 | 256 | return ev 257 | } 258 | 259 | async newProjectEvaluator(projectDir: string, opts: EvaluatorOptions): Promise { 260 | const projectEvaluator = await this.newEvaluator(PreconfiguredOptions) 261 | const project = await loadProjectFromEvaluator(projectEvaluator, projectDir + "/PklProject") 262 | 263 | return this.newEvaluator({...withProject(project), ...opts}) 264 | } 265 | } 266 | 267 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /examples/basic-intro/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-intro", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "basic-intro", 9 | "version": "0.0.1", 10 | "dependencies": { 11 | "pkl-typescript": "file:../..", 12 | "tsx": "^4.7.1", 13 | "typescript": "^5.3.3" 14 | } 15 | }, 16 | "../..": { 17 | "name": "@pkl-community/pkl-typescript", 18 | "version": "0.0.2", 19 | "license": "Apache-2.0", 20 | "dependencies": { 21 | "chalk": "^4.1.2", 22 | "cmd-ts": "^0.13.0", 23 | "consola": "^3.2.3", 24 | "msgpackr": "^1.10.1" 25 | }, 26 | "bin": { 27 | "pkl-gen-typescript": "bin/pkl-gen-typescript.ts" 28 | }, 29 | "devDependencies": { 30 | "@jest/globals": "^29.7.0", 31 | "@types/node": "^18.19.18", 32 | "@typescript-eslint/eslint-plugin": "^5.48.0", 33 | "@typescript-eslint/parser": "^5.48.0", 34 | "eslint": "^8.31.0", 35 | "jest": "^29.7.0", 36 | "ts-jest": "^29.1.2", 37 | "tsx": "^4.7.1", 38 | "typescript": "^4.9.4" 39 | } 40 | }, 41 | "node_modules/@esbuild/aix-ppc64": { 42 | "version": "0.19.12", 43 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", 44 | "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", 45 | "cpu": [ 46 | "ppc64" 47 | ], 48 | "optional": true, 49 | "os": [ 50 | "aix" 51 | ], 52 | "engines": { 53 | "node": ">=12" 54 | } 55 | }, 56 | "node_modules/@esbuild/android-arm": { 57 | "version": "0.19.12", 58 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", 59 | "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", 60 | "cpu": [ 61 | "arm" 62 | ], 63 | "optional": true, 64 | "os": [ 65 | "android" 66 | ], 67 | "engines": { 68 | "node": ">=12" 69 | } 70 | }, 71 | "node_modules/@esbuild/android-arm64": { 72 | "version": "0.19.12", 73 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", 74 | "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", 75 | "cpu": [ 76 | "arm64" 77 | ], 78 | "optional": true, 79 | "os": [ 80 | "android" 81 | ], 82 | "engines": { 83 | "node": ">=12" 84 | } 85 | }, 86 | "node_modules/@esbuild/android-x64": { 87 | "version": "0.19.12", 88 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", 89 | "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", 90 | "cpu": [ 91 | "x64" 92 | ], 93 | "optional": true, 94 | "os": [ 95 | "android" 96 | ], 97 | "engines": { 98 | "node": ">=12" 99 | } 100 | }, 101 | "node_modules/@esbuild/darwin-arm64": { 102 | "version": "0.19.12", 103 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", 104 | "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", 105 | "cpu": [ 106 | "arm64" 107 | ], 108 | "optional": true, 109 | "os": [ 110 | "darwin" 111 | ], 112 | "engines": { 113 | "node": ">=12" 114 | } 115 | }, 116 | "node_modules/@esbuild/darwin-x64": { 117 | "version": "0.19.12", 118 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", 119 | "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", 120 | "cpu": [ 121 | "x64" 122 | ], 123 | "optional": true, 124 | "os": [ 125 | "darwin" 126 | ], 127 | "engines": { 128 | "node": ">=12" 129 | } 130 | }, 131 | "node_modules/@esbuild/freebsd-arm64": { 132 | "version": "0.19.12", 133 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", 134 | "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", 135 | "cpu": [ 136 | "arm64" 137 | ], 138 | "optional": true, 139 | "os": [ 140 | "freebsd" 141 | ], 142 | "engines": { 143 | "node": ">=12" 144 | } 145 | }, 146 | "node_modules/@esbuild/freebsd-x64": { 147 | "version": "0.19.12", 148 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", 149 | "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", 150 | "cpu": [ 151 | "x64" 152 | ], 153 | "optional": true, 154 | "os": [ 155 | "freebsd" 156 | ], 157 | "engines": { 158 | "node": ">=12" 159 | } 160 | }, 161 | "node_modules/@esbuild/linux-arm": { 162 | "version": "0.19.12", 163 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", 164 | "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", 165 | "cpu": [ 166 | "arm" 167 | ], 168 | "optional": true, 169 | "os": [ 170 | "linux" 171 | ], 172 | "engines": { 173 | "node": ">=12" 174 | } 175 | }, 176 | "node_modules/@esbuild/linux-arm64": { 177 | "version": "0.19.12", 178 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", 179 | "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", 180 | "cpu": [ 181 | "arm64" 182 | ], 183 | "optional": true, 184 | "os": [ 185 | "linux" 186 | ], 187 | "engines": { 188 | "node": ">=12" 189 | } 190 | }, 191 | "node_modules/@esbuild/linux-ia32": { 192 | "version": "0.19.12", 193 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", 194 | "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", 195 | "cpu": [ 196 | "ia32" 197 | ], 198 | "optional": true, 199 | "os": [ 200 | "linux" 201 | ], 202 | "engines": { 203 | "node": ">=12" 204 | } 205 | }, 206 | "node_modules/@esbuild/linux-loong64": { 207 | "version": "0.19.12", 208 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", 209 | "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", 210 | "cpu": [ 211 | "loong64" 212 | ], 213 | "optional": true, 214 | "os": [ 215 | "linux" 216 | ], 217 | "engines": { 218 | "node": ">=12" 219 | } 220 | }, 221 | "node_modules/@esbuild/linux-mips64el": { 222 | "version": "0.19.12", 223 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", 224 | "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", 225 | "cpu": [ 226 | "mips64el" 227 | ], 228 | "optional": true, 229 | "os": [ 230 | "linux" 231 | ], 232 | "engines": { 233 | "node": ">=12" 234 | } 235 | }, 236 | "node_modules/@esbuild/linux-ppc64": { 237 | "version": "0.19.12", 238 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", 239 | "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", 240 | "cpu": [ 241 | "ppc64" 242 | ], 243 | "optional": true, 244 | "os": [ 245 | "linux" 246 | ], 247 | "engines": { 248 | "node": ">=12" 249 | } 250 | }, 251 | "node_modules/@esbuild/linux-riscv64": { 252 | "version": "0.19.12", 253 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", 254 | "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", 255 | "cpu": [ 256 | "riscv64" 257 | ], 258 | "optional": true, 259 | "os": [ 260 | "linux" 261 | ], 262 | "engines": { 263 | "node": ">=12" 264 | } 265 | }, 266 | "node_modules/@esbuild/linux-s390x": { 267 | "version": "0.19.12", 268 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", 269 | "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", 270 | "cpu": [ 271 | "s390x" 272 | ], 273 | "optional": true, 274 | "os": [ 275 | "linux" 276 | ], 277 | "engines": { 278 | "node": ">=12" 279 | } 280 | }, 281 | "node_modules/@esbuild/linux-x64": { 282 | "version": "0.19.12", 283 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", 284 | "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", 285 | "cpu": [ 286 | "x64" 287 | ], 288 | "optional": true, 289 | "os": [ 290 | "linux" 291 | ], 292 | "engines": { 293 | "node": ">=12" 294 | } 295 | }, 296 | "node_modules/@esbuild/netbsd-x64": { 297 | "version": "0.19.12", 298 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", 299 | "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", 300 | "cpu": [ 301 | "x64" 302 | ], 303 | "optional": true, 304 | "os": [ 305 | "netbsd" 306 | ], 307 | "engines": { 308 | "node": ">=12" 309 | } 310 | }, 311 | "node_modules/@esbuild/openbsd-x64": { 312 | "version": "0.19.12", 313 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", 314 | "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", 315 | "cpu": [ 316 | "x64" 317 | ], 318 | "optional": true, 319 | "os": [ 320 | "openbsd" 321 | ], 322 | "engines": { 323 | "node": ">=12" 324 | } 325 | }, 326 | "node_modules/@esbuild/sunos-x64": { 327 | "version": "0.19.12", 328 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", 329 | "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", 330 | "cpu": [ 331 | "x64" 332 | ], 333 | "optional": true, 334 | "os": [ 335 | "sunos" 336 | ], 337 | "engines": { 338 | "node": ">=12" 339 | } 340 | }, 341 | "node_modules/@esbuild/win32-arm64": { 342 | "version": "0.19.12", 343 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", 344 | "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", 345 | "cpu": [ 346 | "arm64" 347 | ], 348 | "optional": true, 349 | "os": [ 350 | "win32" 351 | ], 352 | "engines": { 353 | "node": ">=12" 354 | } 355 | }, 356 | "node_modules/@esbuild/win32-ia32": { 357 | "version": "0.19.12", 358 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", 359 | "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", 360 | "cpu": [ 361 | "ia32" 362 | ], 363 | "optional": true, 364 | "os": [ 365 | "win32" 366 | ], 367 | "engines": { 368 | "node": ">=12" 369 | } 370 | }, 371 | "node_modules/@esbuild/win32-x64": { 372 | "version": "0.19.12", 373 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", 374 | "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", 375 | "cpu": [ 376 | "x64" 377 | ], 378 | "optional": true, 379 | "os": [ 380 | "win32" 381 | ], 382 | "engines": { 383 | "node": ">=12" 384 | } 385 | }, 386 | "node_modules/esbuild": { 387 | "version": "0.19.12", 388 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", 389 | "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", 390 | "hasInstallScript": true, 391 | "bin": { 392 | "esbuild": "bin/esbuild" 393 | }, 394 | "engines": { 395 | "node": ">=12" 396 | }, 397 | "optionalDependencies": { 398 | "@esbuild/aix-ppc64": "0.19.12", 399 | "@esbuild/android-arm": "0.19.12", 400 | "@esbuild/android-arm64": "0.19.12", 401 | "@esbuild/android-x64": "0.19.12", 402 | "@esbuild/darwin-arm64": "0.19.12", 403 | "@esbuild/darwin-x64": "0.19.12", 404 | "@esbuild/freebsd-arm64": "0.19.12", 405 | "@esbuild/freebsd-x64": "0.19.12", 406 | "@esbuild/linux-arm": "0.19.12", 407 | "@esbuild/linux-arm64": "0.19.12", 408 | "@esbuild/linux-ia32": "0.19.12", 409 | "@esbuild/linux-loong64": "0.19.12", 410 | "@esbuild/linux-mips64el": "0.19.12", 411 | "@esbuild/linux-ppc64": "0.19.12", 412 | "@esbuild/linux-riscv64": "0.19.12", 413 | "@esbuild/linux-s390x": "0.19.12", 414 | "@esbuild/linux-x64": "0.19.12", 415 | "@esbuild/netbsd-x64": "0.19.12", 416 | "@esbuild/openbsd-x64": "0.19.12", 417 | "@esbuild/sunos-x64": "0.19.12", 418 | "@esbuild/win32-arm64": "0.19.12", 419 | "@esbuild/win32-ia32": "0.19.12", 420 | "@esbuild/win32-x64": "0.19.12" 421 | } 422 | }, 423 | "node_modules/fsevents": { 424 | "version": "2.3.3", 425 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 426 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 427 | "hasInstallScript": true, 428 | "optional": true, 429 | "os": [ 430 | "darwin" 431 | ], 432 | "engines": { 433 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 434 | } 435 | }, 436 | "node_modules/get-tsconfig": { 437 | "version": "4.7.3", 438 | "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz", 439 | "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==", 440 | "dependencies": { 441 | "resolve-pkg-maps": "^1.0.0" 442 | }, 443 | "funding": { 444 | "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" 445 | } 446 | }, 447 | "node_modules/pkl-typescript": { 448 | "resolved": "../..", 449 | "link": true 450 | }, 451 | "node_modules/resolve-pkg-maps": { 452 | "version": "1.0.0", 453 | "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", 454 | "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", 455 | "funding": { 456 | "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" 457 | } 458 | }, 459 | "node_modules/tsx": { 460 | "version": "4.7.1", 461 | "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.1.tgz", 462 | "integrity": "sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==", 463 | "dependencies": { 464 | "esbuild": "~0.19.10", 465 | "get-tsconfig": "^4.7.2" 466 | }, 467 | "bin": { 468 | "tsx": "dist/cli.mjs" 469 | }, 470 | "engines": { 471 | "node": ">=18.0.0" 472 | }, 473 | "optionalDependencies": { 474 | "fsevents": "~2.3.3" 475 | } 476 | }, 477 | "node_modules/typescript": { 478 | "version": "5.4.2", 479 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", 480 | "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", 481 | "bin": { 482 | "tsc": "bin/tsc", 483 | "tsserver": "bin/tsserver" 484 | }, 485 | "engines": { 486 | "node": ">=14.17" 487 | } 488 | } 489 | } 490 | } 491 | --------------------------------------------------------------------------------