inheriting imports on amends / extends #323

Open
opened 2025-12-30 01:23:32 +01:00 by adam · 3 comments
Owner

Originally created by @jjuliano on GitHub (Jul 25, 2025).

Hello,

I have a pkl template file that have many imports defined. For example, main.pkl is my template, and foo.pkl is a supporting util library, and bar.pkl is my configuration file.

The idea is that, as a user-facing configuration file, for UX reasons, bar.pkl shouldn't have 100 import lines, where the user scrolls to the bottom of the file to enter their configurations.

I assume that since the imports are already in the non user-facing pkl files, then by doing 'amends' or 'extends' the config files can just access those imports.

At the moment, I did several test for this functionality as seen below. Is there another way to do this without writing a chaining function on the template?

main.pkl

open module main

import "foo.pkl" as foo

bar: String?
baz: String?

function foo1(): String = foo.foo()

foo.pkl

function foo(): String = "foo bar baz"

bar.pkl

amends "main.pkl"

bar = foo1()
baz = "\(foo.foo())"
$ pkl eval bar.pkl
–– Pkl Error ––
Cannot find property `foo`.

4 | baz = "\(foo.foo())"
             ^^^
at bar#baz (file:///private/tmp/inherited-imports/bar.pkl, line 4)

Did you mean any of the following?
foo1()

106 | text = renderer.renderDocument(value)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

baz.pkl

extends "main.pkl"

bar = foo1()
baz = "\(foo.foo())"
$ pkl eval baz.pkl

–– Pkl Error ––
Cannot find property `foo`.

4 | baz = "\(foo.foo())"
             ^^^
at baz#baz (file:///private/tmp/inherited-imports/baz.pkl, line 4)

Did you mean any of the following?
foo1()

106 | text = renderer.renderDocument(value)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

qux.pkl

amends "main.pkl"

bar = foo1()
$ pkl eval qux.pkl

bar = "foo bar baz"
baz = null
Originally created by @jjuliano on GitHub (Jul 25, 2025). Hello, I have a pkl template file that have many imports defined. For example, main.pkl is my template, and foo.pkl is a supporting util library, and bar.pkl is my configuration file. The idea is that, as a user-facing configuration file, for UX reasons, bar.pkl shouldn't have 100 import lines, where the user scrolls to the bottom of the file to enter their configurations. I assume that since the imports are already in the non user-facing pkl files, then by doing 'amends' or 'extends' the config files can just access those imports. At the moment, I did several test for this functionality as seen below. Is there another way to do this without writing a chaining function on the template? *main.pkl* ```pkl open module main import "foo.pkl" as foo bar: String? baz: String? function foo1(): String = foo.foo() ``` *foo.pkl* ```pkl function foo(): String = "foo bar baz" ``` *bar.pkl* ```pkl amends "main.pkl" bar = foo1() baz = "\(foo.foo())" ``` ```sh $ pkl eval bar.pkl –– Pkl Error –– Cannot find property `foo`. 4 | baz = "\(foo.foo())" ^^^ at bar#baz (file:///private/tmp/inherited-imports/bar.pkl, line 4) Did you mean any of the following? foo1() 106 | text = renderer.renderDocument(value) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` *baz.pkl* ```pkl extends "main.pkl" bar = foo1() baz = "\(foo.foo())" ``` ```sh $ pkl eval baz.pkl –– Pkl Error –– Cannot find property `foo`. 4 | baz = "\(foo.foo())" ^^^ at baz#baz (file:///private/tmp/inherited-imports/baz.pkl, line 4) Did you mean any of the following? foo1() 106 | text = renderer.renderDocument(value) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` *qux.pkl* ```pkl amends "main.pkl" bar = foo1() ``` ```sh $ pkl eval qux.pkl bar = "foo bar baz" baz = null ```
Author
Owner

@bioball commented on GitHub (Jul 25, 2025):

You can expose the import as a property. This makes makes it a hidden const, which means:

  1. The property does not get rendered tot JSON/YAML/etc, nor exported to Go/Java/Swift
  2. The property cannot be assigned to in an amending/extending module
// bar.pkl
hidden const foo = import("foo.pkl")

Now, you can reference foo plainly in the amending/extending module. If you're in a nested scope, you need to use the module keyword:

// baz.pkl
amends "bar.pkl"

baz = foo.foo()

nested {
  qux = module.foo.foo()
}

Note: an import gives you both a value and a type. If you want to expose imported types to a child module, you can create a typealias:

// bar.pkl
import "MyType.pkl" as _MyType

typealias MyType = _MyType
amends "bar.pkl"

local t: MyType = new {}
@bioball commented on GitHub (Jul 25, 2025): You can expose the import as a property. This makes makes it a `hidden const`, which means: 1. The property does not get rendered tot JSON/YAML/etc, nor exported to Go/Java/Swift 2. The property cannot be assigned to in an amending/extending module ```pkl // bar.pkl hidden const foo = import("foo.pkl") ``` Now, you can reference `foo` plainly in the amending/extending module. If you're in a nested scope, you need to use the `module` keyword: ```pkl // baz.pkl amends "bar.pkl" baz = foo.foo() nested { qux = module.foo.foo() } ``` Note: an import gives you both a value and a type. If you want to expose imported _types_ to a child module, you can create a typealias: ```pkl // bar.pkl import "MyType.pkl" as _MyType typealias MyType = _MyType ``` ```pkl amends "bar.pkl" local t: MyType = new {} ```
Author
Owner

@jjuliano commented on GitHub (Jul 26, 2025):

You can expose the import as a property. This makes makes it a hidden const, which means:

  1. The property does not get rendered tot JSON/YAML/etc, nor exported to Go/Java/Swift
  2. The property cannot be assigned to in an amending/extending module

// bar.pkl
hidden const foo = import("foo.pkl")
Now, you can reference foo plainly in the amending/extending module. If you're in a nested scope, you need to use the module keyword:

// baz.pkl
amends "bar.pkl"

baz = foo.foo()

nested {
qux = module.foo.foo()
}
Note: an import gives you both a value and a type. If you want to expose imported types to a child module, you can create a typealias:

// bar.pkl
import "MyType.pkl" as _MyType

typealias MyType = _MyType
amends "bar.pkl"

local t: MyType = new {}

Thanks @bioball, this works, however, for the nested, is it possible to not have to call module to access the function?

@jjuliano commented on GitHub (Jul 26, 2025): > You can expose the import as a property. This makes makes it a `hidden const`, which means: > > 1. The property does not get rendered tot JSON/YAML/etc, nor exported to Go/Java/Swift > 2. The property cannot be assigned to in an amending/extending module > > // bar.pkl > hidden const foo = import("foo.pkl") > Now, you can reference `foo` plainly in the amending/extending module. If you're in a nested scope, you need to use the `module` keyword: > > // baz.pkl > amends "bar.pkl" > > baz = foo.foo() > > nested { > qux = module.foo.foo() > } > Note: an import gives you both a value and a type. If you want to expose imported _types_ to a child module, you can create a typealias: > > // bar.pkl > import "MyType.pkl" as _MyType > > typealias MyType = _MyType > amends "bar.pkl" > > local t: MyType = new {} Thanks @bioball, this works, however, for the nested, is it possible to not have to call `module` to access the function?
Author
Owner

@bioball commented on GitHub (Jul 26, 2025):

This is an intentional rule; it guarantees that adding/removing names in the base module will not change the meaning of a name lookup in an amending/extending module.

You can read more about this rationale here: https://github.com/apple/pkl/discussions/865#discussioncomment-11640312

@bioball commented on GitHub (Jul 26, 2025): This is an intentional rule; it guarantees that adding/removing names in the base module will not change the meaning of a name lookup in an amending/extending module. You can read more about this rationale here: https://github.com/apple/pkl/discussions/865#discussioncomment-11640312
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/pkl#323