Typescript Module Configuration
Common Configuration Overrides
package build
import mill._
import mill.javascriptlib._
object foo extends TypeScriptModule {
def customSource = Task {
Seq(PathRef(millSourcePath / "custom-src" / "foo2.ts"))
}
def allSources = super.allSources() ++ customSource()
def resources = super.resources() ++ Seq(PathRef(millSourcePath / "custom-resources"))
def generatedSources = Task {
for (name <- Seq("A", "B", "C")) os.write(
Task.dest / s"foo-$name.ts",
s"""export default class Foo$name {
static value: string = "Hello $name"
}
""".stripMargin
)
Seq(PathRef(Task.dest))
}
def forkEnv = super.forkEnv() + ("MY_CUSTOM_ENV" -> "my-env-value")
def mainFileName = s"foo2.ts"
def mainFilePath = compile()._2.path / "custom-src" / mainFileName()
}
This example demonstrates usage of common configs
Note the use of millSourcePath
, Task.dest
, and PathRef
when preforming
various filesystem operations:
-
millSourcePath
: Base path of the module. For the root module, it’s the repo root. For inner modules, it’s the module path (e.g.,foo/bar/qux
forfoo.bar.qux
). Can be overridden if needed. -
Task.dest
: Destination folder in theout/
folder for task output. Prevents filesystem conflicts and serves as temporary storage or output for tasks. -
PathRef
: Represents the contents of a file or folder, not just its path, ensuring downstream tasks properly invalidate when contents change.
Typical Usage is given below:
> mill foo.run
hello2
Hello A
Hello B
Hello C
my-env-value
MyResource: My Resource Contents
> mill foo.bundle
Build succeeded!
> MY_CUSTOM_ENV=my-env-value node out/foo/bundle.dest/bundle.js
hello2
Hello A
Hello B
Hello C
my-env-value
MyResource: My Resource Contents
MyOtherResource: My Other Resource Contents
Custom Tasks
package build
import mill._, javascriptlib._
object foo extends TypeScriptModule {
/** Total number of lines in module source files */
def lineCount = Task {
allSources().map(f => os.read.lines(f.path).size).sum
}
def generatedSources = Task {
os.write(
Task.dest / "bar.ts",
s"""export default class Bar {
static value: number = 123
}
""".stripMargin
)
Seq(PathRef(Task.dest))
}
def forkEnv = super.forkEnv() + ("LINE_COUNT" -> lineCount().toString)
def printLineCount() = Task.Command { println(lineCount()) }
}
The above build defines the customizations to the Mill task graph shown below, with the boxes representing tasks defined or overriden above and the un-boxed labels representing existing Mill tasks:
Mill lets you define new cached Tasks using the Task {…}
syntax,
depending on existing Tasks e.g. foo.sources
via the foo.sources()
syntax to extract their current value, as shown in lineCount
above. The
return-type of a Task has to be JSON-serializable (using
uPickle, one of Mill’s Bundled Libraries)
and the Task is cached when first run until its inputs change (in this case, if
someone edits the foo.sources
files which live in foo/src
). Cached Tasks
cannot take parameters.
Note that depending on a task requires use of parentheses after the task
name, e.g. pythonDeps()
, sources()
and lineCount()
. This converts the
task of type T[V]
into a value of type V
you can make use in your task
implementation.
> mill foo.run "Hello World!"
Bar.value: 123
text: Hello World!
Line count: 13
Overriding Tasks
package build
import mill._
import mill.javascriptlib._
object foo extends TypeScriptModule {
def sources = Task {
val srcPath = Task.dest / "src"
val filePath = srcPath / "foo.ts"
os.makeDir.all(srcPath)
os.write.over(
filePath,
"""(function () {
console.log("Hello World!")
})()
""".stripMargin
)
PathRef(Task.dest)
}
def compile = Task {
println("Compiling...")
os.copy(sources().path, super.compile()._2.path, mergeFolders = true)
super.compile()
}
def run(args: mill.define.Args) = Task.Command {
println("Running... " + args.value.mkString(" "))
super.run(args)()
}
}
You can re-define tasks to override them, and use super
if you
want to refer to the originally defined task. The above example shows how to
override compile
and run
to add additional logging messages, and we
override sources
which was Task.Sources
for the src/
folder with a plain
T{…}
task that generates the necessary source files on-the-fly.
that this example replaces your src/ folder with the generated
sources, as we are overriding the def sources task. If you want to add
generated sources, you can either override generatedSources , or you can
override sources and use super to include the original source folder with super :
|
> mill foo.run "added tags"
Compiling...
Hello World!
Running... added tags
Compilation & Execution Flags
package build
import mill._
import mill.javascriptlib._
object foo extends TypeScriptModule {
def compilerOptions =
super.compilerOptions() + (
"skipLibCheck" -> ujson.True,
"module" -> ujson.Str("commonjs"),
"target" -> ujson.Str("es6")
)
def executionFlags =
Map(
"inspect" -> "",
"max-old-space-size" -> "4096"
)
}
This example demonstrates defining typescript compiler options and node.js execution flags
> mill foo.run
Debugger listening on ws://...
...
Foo:
Hello World!
Filesystem Resources
package build
import mill._, javascriptlib._
import ujson._
object foo extends TypeScriptModule {
object test extends TypeScriptTests with TestModule.Jest {
def otherFiles = T.source(millSourcePath / "other-files")
def forkEnv = super.forkEnv() + ("OTHER_FILES_DIR" -> otherFiles().path.toString)
}
}
> mill foo.run
Hello World Resource File
> mill foo.test
PASS .../foo.test.ts
...
Test Suites:...1 passed, 1 total...
Tests:...3 passed, 3 total...
...
> mill foo.bundle
Build succeeded!
> mv out/foo/bundle.dest ./bundle.dest && rm -rf out/
> node ./bundle.dest/bundle.js
Hello World Resource File
This section discusses how tests can depend on resources locally on disk.
Mill provides two ways to do this: via ts-config paths, and via
the resource folder which is made available as the environment variable
MILL_TEST_RESOURCE_DIR
;
-
The ts-config paths resources are useful when you want to fetch individual files, and are bundled with the application by the
.bundle
step when bundling code for deployment. But they do not allow you to list folders or perform other filesystem operations. -
The resource folder, available via
MILL_TEST_RESOURCE_DIR
, gives you access to the folder path of the resources on disk. This is useful in allowing you to list and otherwise manipulate the filesystem, which you cannot do with tsconfig-paths resources. However, theMILL_TEST_RESOURCE_DIR
only exists when running tests using Mill, and is not available when executing applications packaged for deployment via.bundle
-
Apart from
resources/
, you can provide additional folders to your test suite by defining aTask.Source
(otherFiles
above) and passing it toforkEnv
. This provide the folder path as an environment variable that the test can make use of
Example application code demonstrating the techniques above can be seen below:
Hello World Resource File
Test Hello World Resource File A
Test Hello World Resource File B
Other Hello World File
Note that tests require that you pass in any files that they depend on explicitly. This is necessary so that Mill knows when a test needs to be re-run and when a previous result can be cached. This also ensures that tests reading and writing to the current working directory do not accidentally interfere with each others files, especially when running in parallel.
Mill runs test processes in a sandbox/ folder, not in your project root folder, to
prevent you from accidentally accessing files without explicitly passing them. Thus
you cannot just read resources off disk via with fs.readFile("foo/resources/test-file-a.txt", {…})
as file.
If you have legacy tests that need to run in the project root folder to work, you
can configure your test suite with def testSandboxWorkingDir = false
to disable
the sandbox and make the tests run in the project root.
Bundling Configuration
package build
import mill._
import mill.javascriptlib._
object foo extends TypeScriptModule {
def bundleFlags =
Map(
"platform" -> ujson.Str("node"),
"entryPoints" -> ujson.Arr(mainFilePath().toString),
"bundle" -> ujson.Bool(true),
"minify" -> ujson.Bool(true)
)
}
The above build demonstrates passing bundle flags
After generating your bundle with mill foo.bundle
you’ll find by running
your out/foo/bundle.dest/bundle.js
you’ll get the programmed output
and usage is based on project logic like which args to include with command.
> mill foo.bundle
Build succeeded!
> node out/foo/bundle.dest/bundle.js
Hello World!